----------------Eggs-It----------------
A 4am crack                  2019-04-14
-------------------. updated 2020-06-24
                   |___________________

Name: Eggs-It
Genre: action
Year: 1982
Credits: Nasir Gebelli
Publisher: Gebelli Software
Protection: Roland Gustafsson
Platform: Apple ][ (48K)
Media: 5.25-inch disk
Sides: 1
OS: custom

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  immediate disk read error

Locksmith Fast Disk Backup
  unable to read any track

EDD 4 bit copy (no sync, no count)
  read errors on tracks $0C+
  copy loads title page then hangs when
  it tries to load the rest

Copy ][+ nibble editor
  appears to be 4-4 encoded data
  no standard sectors or structure

Why didn't COPYA work?
  not a 16-sector disk

Why didn't Locksmith FDB work?
  not a 16-sector disk

Why didn't my EDD copy work?
  I don't know. Half tracks, maybe?
  The disk sounds like it's seeking to
  successive tracks very quickly, so
  maybe it's doing a spiral-y kind of
  thing. Hard to tell just on sound
  alone though.

This is decidedly not a single-load
game. There is a classic crack that is
a single binary, but it cuts out the
initial animated title page.

Combined with the early indications of
a custom bootloader and 4-4 encoded
sectors, this is not going to be a
straightforward crack by any definition
of "straight" or "forward."

Let's start at the beginning.

                   ~

               Chapter 1
      In Which We Brag About Our
           Humble Beginnings


I have two floppy drives, one in slot 6
and the other in slot 5. My "work disk"
(in slot 5) runs Diversi-DOS 64K, which
is compatible with Apple DOS 3.3 but
relocates most of DOS to the language
card on boot. This frees up most of
main memory (only using a single page
at $BF00..$BFFF), which is useful for
loading large files or examining code
that lives in areas typically reserved
for DOS.

[S6,D1=original disk]
[S5,D1=my work disk]

The floppy drive firmware code at $C600
is responsible for aligning the drive
head and reading sector 0 of track 0
into main memory at $0800. Because the
drive can be connected to any slot, the
firmware code can't assume it's loaded
at $C600. If the floppy drive card were
removed from slot 6 and reinstalled in
slot 5, the firmware code would load at
$C500 instead.

To accommodate this, the firmware does
some fancy stack manipulation to detect
where it is in memory (which is a neat
trick, since the 6502 program counter
is not generally accessible). However,
due to space constraints, the detection
code only cares about the lower 4 bits
of the high byte of its own address.

$C600 (or $C500, or anywhere in $Cx00)
is read-only memory. I can't change it,
which means I can't stop it from
transferring control to the boot sector
of the disk once it's in memory. BUT!
The disk firmware code works unmodified
at any address. Any address that ends
with $x600 will boot slot 6, including
$B600, $A600, $9600, &c.

; copy drive firmware to $9600
*9600<C600.C6FFM

; and execute it
*9600G
...reboots slot 6, loads game...

Thus:

]PR#5
...
]CALL -151

*9600<C600.C6FFM

*96F8L

96F8-   4C 01 08    JMP   $0801

That's where the disk controller ROM
code ends and the on-disk code begins.
But $9600 is part of read/write memory.
I can change it at will. So I can
interrupt the boot process after the
drive firmware loads the boot sector
from the disk but before it transfers
control to the disk's bootloader.

; instead of jumping to on-disk code,
; copy boot sector to higher memory so
; it survives a reboot
96F8-   A0 00       LDY   #$00
96FA-   B9 00 08    LDA   $0800,Y
96FD-   99 00 28    STA   $2800,Y
9700-   C8          INY
9701-   D0 F7       BNE   $96FA

; turn off slot 6 drive motor
9703-   AD E8 C0    LDA   $C0E8

; reboot to my work disk in slot 5
9706-   4C 00 C5    JMP   $C500

*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0800-08FF,A$2800,L$100

Now we get to(*) trace the boot process
one sector, one page, one instruction
at a time.

(*) If you replace the words "need to"
    with the words "get to," life
    becomes amazing.

                   ~

               Chapter 2
  Different Yolks For Different Folks


]CALL -151

; move bootloader back into place
*800<2800.28FFM

*801L

; the bootloader immediately moves
; itself to the input buffer at $0200
0801-   A2 00       LDX   #$00
0803-   BD 00 08    LDA   $0800,X
0806-   9D 00 02    STA   $0200,X
0809-   E8          INX
080A-   D0 F7       BNE   $0803
080C-   4C 0F 02    JMP   $020F

*20F<80F.8FFM

; set up a nibble table at $0800
020F-   A0 AB       LDY   #$AB
0211-   98          TYA
0212-   85 3C       STA   $3C
0214-   4A          LSR
0215-   05 3C       ORA   $3C
0217-   C9 FF       CMP   #$FF
0219-   D0 09       BNE   $0224
021B-   C0 D5       CPY   #$D5
021D-   F0 05       BEQ   $0224
021F-   8A          TXA
0220-   99 00 08    STA   $0800,Y
0223-   E8          INX
0224-   C8          INY
0225-   D0 EA       BNE   $0211
0227-   84 3D       STY   $3D

; $00 into zero page $26 and $03 into
; $27 means we're probably going to be
; loading data into $0300..$03FF later.
0229-   84 26       STY   $26
022B-   A9 03       LDA   #$03
022D-   85 27       STA   $27
022F-   A6 2B       LDX   $2B
0231-   20 5D 02    JSR   $025D

*25DL

; read a sector from track $00 (this is
; actually derived from the code in the
; disk controller ROM routine at $C65C,
; but looking for an address prologue
; of "D5 AA B5" instead of "D5 AA 96")
; and using the nibble table we set up
; earlier at $0800
025D-   18          CLC
025E-   08          PHP
025F-   BD 8C C0    LDA   $C08C,X
0262-   10 FB       BPL   $025F
0264-   49 D5       EOR   #$D5
0266-   D0 F7       BNE   $025F
0268-   BD 8C C0    LDA   $C08C,X
026B-   10 FB       BPL   $0268
026D-   C9 AA       CMP   #$AA
026F-   D0 F3       BNE   $0264
0271-   EA          NOP
0272-   BD 8C C0    LDA   $C08C,X
0275-   10 FB       BPL   $0272
0277-   C9 B5       CMP   #$B5
0279-   F0 09       BEQ   $0284
027B-   28          PLP
027C-   90 DF       BCC   $025D
027E-   49 AD       EOR   #$AD
0280-   F0 1F       BEQ   $02A1
0282-   D0 D9       BNE   $025D
0284-   A0 03       LDY   #$03
0286-   84 2A       STY   $2A
0288-   BD 8C C0    LDA   $C08C,X
028B-   10 FB       BPL   $0288
028D-   2A          ROL
028E-   85 3C       STA   $3C
0290-   BD 8C C0    LDA   $C08C,X
0293-   10 FB       BPL   $0290
0295-   25 3C       AND   $3C
0297-   88          DEY
0298-   D0 EE       BNE   $0288
029A-   28          PLP
029B-   C5 3D       CMP   $3D
029D-   D0 BE       BNE   $025D
029F-   B0 BD       BCS   $025E
02A1-   A0 9A       LDY   #$9A
02A3-   84 3C       STY   $3C
02A5-   BC 8C C0    LDY   $C08C,X
02A8-   10 FB       BPL   $02A5

; use the nibble table we set up
02AA-   59 00 08    EOR   $0800,Y
02AD-   A4 3C       LDY   $3C
02AF-   88          DEY
02B0-   99 00 08    STA   $0800,Y
02B3-   D0 EE       BNE   $02A3
02B5-   84 3C       STY   $3C
02B7-   BC 8C C0    LDY   $C08C,X
02BA-   10 FB       BPL   $02B7
02BC-   59 00 08    EOR   $0800,Y
02BF-   A4 3C       LDY   $3C

; store in $0300
02C1-   91 26       STA   ($26),Y
02C3-   C8          INY
02C4-   D0 EF       BNE   $02B5
02C6-   BC 8C C0    LDY   $C08C,X
02C9-   10 FB       BPL   $02C6
02CB-   59 00 08    EOR   $0800,Y
02CE-   D0 8D       BNE   $025D
02D0-   60          RTS

I've seen this routine, or some slight
variation, on many other disks. It
serves a specific purpose: allowing the
disk to boot on both older 13-sector
drives and "newer" 16-sector drives. In
the early days of the Apple II, Apple
transitioned from drives that could
only read 13 sectors per track to "new
and improved" drives that could read 16
whole sectors per track. This disk, and
others like it, can boot on either kind
of drive because it has two boot
sectors: one for 13-sector drives and
another (which I found here) for
16-sector drives. What we're seeing
here is a shim for "newer" drives to go
load the boot sector that the older
drives would load automatically.

Continuing from $0237...

*237L

0237-   4C 01 03    JMP   $0301

And that's where I get to interrupt the
boot.

*9600<C600.C6FFM

96F8-   A9 05       LDA   #$05
96FA-   8D 38 08    STA   $0838
96FD-   A9 97       LDA   #$97
96FF-   8D 39 08    STA   $0839
9702-   4C 01 08    JMP   $0801
9705-   A0 00       LDY   #$00
9707-   B9 00 03    LDA   $0300,Y
970A-   99 00 23    STA   $2300,Y
970D-   C8          INY
970E-   D0 F7       BNE   $9707
9710-   AD E8 C0    LDA   $C0E8
9713-   4C 00 C5    JMP   $C500

*BSAVE TRACE1,A$9600,L$116
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0300-03FF,A$2300,L$100

                   ~

               Chapter 3
 In Which We Try To Unscramble An Egg
      And Learn Why That's Not A
            Popular Saying


*2301L

; set stack pointer
2301-   A2 2E       LDX   #$2E
2303-   9A          TXS

; now this entire thing is a loop that
; reads this page (including the loop
; code itself), perturbs it in some
; deterministic way, and writes it
; directly to the stack page at $0100
2304-   84 48       STY   $48
2306-   A2 00       LDX   #$00
2308-   BC 00 03    LDY   $0300,X
230B-   88          DEY
230C-   A9 EA       LDA   #$EA
230E-   20 26 03    JSR   $0326
2311-   C8          INY
2312-   A5 48       LDA   $48
2314-   20 26 03    JSR   $0326
2317-   88          DEY
2318-   A5 48       LDA   $48
231A-   20 26 03    JSR   $0326
231D-   88          DEY
231E-   98          TYA
231F-   9D 00 01    STA   $0100,X
2322-   E8          INX
2323-   D0 E3       BNE   $2308

; exit via "RTS", which will take an
; address off the stack (which is the
; page at $0100, which we just wrote)
; based on the stack pointer, which we
; just set before the decryption loop
2325-   60          RTS
2326-   85 00       STA   $00
2328-   98          TYA
2329-   45 00       EOR   $00
232B-   A8          TAY
232C-   60          RTS

; everything after this is encrypted
232D-   EA          NOP
232E-   FF          ???
232F-   C6 EF       DEC   $EF
2331-   4C B6 68    JMP   $68B6
2334-   E9 EF       SBC   #$EF
2336-   76 16       ROR   $16,X
...

The question before us: how do we
interrupt this code so we can regain
control of the boot process and capture
the decrypted code on the stack?

We can't modify the decryption code --
even to change the STA at $031F to
write out the decrypted code to another
location -- because the byte at $0321
is part of the decrypted code. (As far
as I can tell, each byte of decrypted
code is independent from the others, so
changing one byte of this code would
"only" result in one byte of decrypted
code being wrong. But still.)

Even if we could modify this code, we
can't change the RTS to JMP because of
the subroutine directly after it; there
is just no room for a JMP. We can't JSR
into the decryption loop at $0304
because the entire stack page is
overwritten. We can't duplicate the
code and run it from another location
because the JSRs at $030E, $0314, and
$031A also modify the stack, and
running it from another location would
mean that those return addresses would
be wrong.

But there is one thing we can do. In
fact, the solution was staring us in
the face the whole time! Just like our
previous trace, we start by copying
page $03 to page $23. Then we can
modify the code on page $03 to read
from page $23 (the LDY at $0308) and
JSR into page $23 (the JSRs at $030E,
$0314, and $031A), modify the RTS at
$0325 to JMP to code under our control,
then JMP $0301 as usual. The loop will
read from an unmodified copy, so the
decrypted code will be exactly as it
would be on the original disk.

After JMPing out, we copy the entire
pristine stack page to somewhere else
(I chose $2100) and reboot to the work
disk.

*9600<C600.C6FFM

; set up callback #1 after reading one
; sector into $0300
96F8-   A9 05       LDA   #$05
96FA-   8D 38 08    STA   $0838
96FD-   A9 97       LDA   #$97
96FF-   8D 39 08    STA   $0839

; start the boot
9702-   4C 01 08    JMP   $0801

; callback #1 --
; copy page $03 to page $23
9705-   A2 00       LDX   #$00
9707-   BD 00 03    LDA   $0300,X
970A-   9D 00 23    STA   $2300,X
970D-   E8          INX
970E-   D0 F7       BNE   $9707

; modify the decryption loop to read
; from and JSR to the pristine copy
; on page $23
9710-   A9 23       LDA   #$23
9712-   8D 0A 03    STA   $030A
9715-   8D 10 03    STA   $0310
9718-   8D 16 03    STA   $0316
971B-   8D 1C 03    STA   $031C

; set up callback #2 after the
; decryption loop is complete (this
; doesn't interfere with the subroutine
; at $0326 because we're not calling
; $0326 anymore, we're calling $2326
; instead!)
971E-   A9 4C       LDA   #$4C
9720-   8D 25 03    STA   $0325
9723-   A9 30       LDA   #$30
9725-   8D 26 03    STA   $0326
9728-   A9 97       LDA   #$97
972A-   8D 27 03    STA   $0327

; continue the boot
972D-   4C 01 03    JMP   $0301

; callback #2 --
; copy the decrypted code from the
; stack page to higher memory
9730-   A0 00       LDY   #$00
9732-   B9 00 01    LDA   $0100,Y
9735-   99 00 21    STA   $2100,Y
9738-   C8          INY
9739-   D0 F7       BNE   $9732

; turn off the drive motor and reboot
; to our work disk
973B-   AD E8 C0    LDA   $C0E8
973E-   4C 00 C5    JMP   $C500

*BSAVE TRACE2,A$9600,L$141
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0100-01FF,A$2100,L$100

                   ~

               Chapter 4
      In Which They Really Tried
        Every Chick In The Book


The stack pointer was set to #$2E (at
$0301), so we can find the "return"
address at $012F, which is in memory at
$212F.

]CALL -151

*212F.2130

212F- 30
2130- 01

That means the "RTS" at $0325 will
"return" to $0131, which is in memory
at $2131.

*2131L

; right off the bat, we're modifying
; the stack page even more
2131-   A2 60       LDX   #$60
2133-   8E FF 01    STX   $01FF

; clear $0400..$0FFF with spaces (that
; includes both text pages, so this
; will visibly clear the screen)
2136-   A0 00       LDY   #$00
2138-   A2 04       LDX   #$04
213A-   84 F0       STY   $F0
213C-   86 F1       STX   $F1
213E-   9A          TXS
213F-   A9 A0       LDA   #$A0
2141-   91 F0       STA   ($F0),Y
2143-   C8          INY
2144-   D0 FB       BNE   $2141
2146-   E6 F1       INC   $F1
2148-   A5 F1       LDA   $F1
214A-   C9 10       CMP   #$10
214C-   90 F1       BCC   $213F

; wipe the language card, if any
214E-   AD 81 C0    LDA   $C081
2151-   AD 81 C0    LDA   $C081
2154-   A9 D0       LDA   #$D0
2156-   85 F1       STA   $F1
2158-   B1 F0       LDA   ($F0),Y
215A-   91 F0       STA   ($F0),Y
215C-   C8          INY
215D-   D0 F9       BNE   $2158
215F-   E6 F1       INC   $F1
2161-   D0 F5       BNE   $2158

; set the low-level reset vector in the
; language card, plus the regular one
; in page 3
2163-   A5 2B       LDA   $2B
2165-   4A          LSR
2166-   4A          LSR
2167-   4A          LSR
2168-   4A          LSR
2169-   09 C0       ORA   #$C0
216B-   8C FC FF    STY   $FFFC
216E-   8D FD FF    STA   $FFFD
2171-   8C F2 03    STY   $03F2
2174-   8D F3 03    STA   $03F3
2177-   49 A5       EOR   #$A5
2179-   8D F4 03    STA   $03F4

; back to ROM
217C-   AD 82 C0    LDA   $C082

; and text page 2 (also just cleared)
217F-   AD 51 C0    LDA   $C051
2182-   AD 55 C0    LDA   $C055

; read RAM bank 2 (so that low-level
; reset vector is active)
2185-   AD 80 C0    LDA   $C080
2188-   20 90 01    JSR   $0190

*2190L

2190-   A2 04       LDX   #$04
2192-   A0 00       LDY   #$00
2194-   84 F7       STY   $F7
2196-   86 F8       STX   $F8
2198-   86 FA       STX   $FA
219A-   A6 2B       LDX   $2B

; subroutine at $01F5 reads one nibble
; from disk, so we're looking for a
; custom prologue $D5 $DD $D5
219C-   20 F5 01    JSR   $01F5
219F-   C9 D5       CMP   #$D5
21A1-   D0 F9       BNE   $219C
21A3-   20 F5 01    JSR   $01F5
21A6-   C9 DD       CMP   #$DD
21A8-   D0 F5       BNE   $219F
21AA-   20 F5 01    JSR   $01F5
21AD-   C9 D5       CMP   #$D5
21AF-   D0 F5       BNE   $21A6

; read 4-4-encoded data into $0400+
; ($F7/$F8 were initialized at $0190)
21B1-   BD 8C C0    LDA   $C08C,X
21B4-   10 FB       BPL   $21B1
21B6-   2A          ROL
21B7-   85 F9       STA   $F9
21B9-   BD 8C C0    LDA   $C08C,X
21BC-   10 FB       BPL   $21B9
21BE-   25 F9       AND   $F9
21C0-   91 F7       STA   ($F7),Y
21C2-   C8          INY
21C3-   D0 EC       BNE   $21B1
21C5-   0E 00 C0    ASL   $C000

; read a one-nibble epilogue after each
; page
21C8-   BD 8C C0    LDA   $C08C,X
21CB-   10 FB       BPL   $21C8
21CD-   C9 DD       CMP   #$DD
21CF-   D0 BF       BNE   $2190

; increment target page in memory and
; decrement sector count, which means
; we'll end up reading 4 sectors into
; $0400..$07FF
21D1-   E6 F8       INC   $F8
21D3-   C6 FA       DEC   $FA
21D5-   D0 DA       BNE   $21B1

; read 4 more nibbles and store them in
; zero page (presumably to be used by
; the code we just read)
21D7-   20 F5 01    JSR   $01F5
21DA-   85 F2       STA   $F2
21DC-   20 F5 01    JSR   $01F5
21DF-   85 F1       STA   $F1
21E1-   20 F5 01    JSR   $01F5
21E4-   85 F0       STA   $F0
21E6-   20 F5 01    JSR   $01F5
21E9-   85 F3       STA   $F3

; one final epilogue, matched against
; one of the nibbles we just read into
; zero page
21EB-   20 F5 01    JSR   $01F5
21EE-   C5 F0       CMP   $F0

; success -> branch
21F0-   F0 09       BEQ   $21FB

; failure -> reboot
21F2-   6C F2 03    JMP   ($03F2)

; (subroutine that reads 1 nibble)
21F5-   BD 8C C0    LDA   $C08C,X
21F8-   10 FB       BPL   $21F5
21FA-   60          RTS

; execution continues here from the
; BEQ at $01F0 -- set the stack pointer
; again
21FB-   AE 65 05    LDX   $0565
21FE-   9A          TXS

; by the time we get here, this has
; been replaced by #$60 (set at $0133),
; which is an RTS
21FF-   00          BRK

Amusing side note: the JSR $0190 never
returns to the caller. If the read
fails, it just reboots (at $01F2); if
it succeeds, we branch to $01FB (from
$01F0) and exit via RTS (at $01FF, set
at $0133) after setting stack pointer
based on code we just read. Under no
circumstances do we execute this code
at $018B:

218B-   A9 04       LDA   #$04
218D-   48          PHA
218E-   48          PHA
218F-   60          RTS

Roland just put that in there because
he had 5 extra bytes and wanted to f---
with people trying to break his code.

Hi Roland.

There is enough room at $01FB to change
the LDX to a JMP and capture the code
that's being stored on the text page.

*9600<C600.C6FFM

; ...code from previous traces omitted
;
; set up callback #3 after the code we
; decrypted onto the stack reads 4 more
; sectors into $0400+
9730-   A9 4C       LDA   #$4C
9732-   8D FB 01    STA   $01FB
9735-   A9 40       LDA   #$40
9737-   8D FC 01    STA   $01FC
973A-   A9 97       LDA   #$97
973C-   8D FD 01    STA   $01FD

; continue the boot (remember, this
; callback was set up after the
; decryption loop at $0300, which sets
; the stack pointer and exits via RTS)
973F-   60          RTS

; callback #3 --
; copy 4 sectors from text page to
; higher memory
9740-   A2 04       LDX   #$04
9742-   A0 00       LDY   #$00
9744-   B9 00 04    LDA   $0400,Y
9747-   99 00 24    STA   $2400,Y
974A-   C8          INY
974B-   D0 F7       BNE   $9744
974D-   EE 46 97    INC   $9746
9750-   EE 49 97    INC   $9749
9753-   CA          DEX
9754-   D0 EE       BNE   $9744

; turn off drive motor and reboot to
; our work disk
9756-   AD E8 C0    LDA   $C0E8
9759-   4C 00 C5    JMP   $C500

*BSAVE TRACE3,A$9600,L$15C
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0400-07FF,A$2400,L$400

                   ~

               Chapter 5
    Frying By The Seat Of Our Pants


The original code at $01FB exited by
setting the stack pointer from the byte
at $0565, which we now have in memory
at $2565.

*2565

2565- 8F

That means we're getting the "return"
address from $0190, which is part of
the page that was decrypted directly
onto the stack.

*BLOAD OBJ.0100-01FF,A$2100

*2190.2191

2190- A2 04

(Note: that's real code at $0190,
because of course it is.)

A return address of $04A2 means that
execution continues at $04A3, which is
in memory at $24A3.

*24A3L

; copy page $05 to page $02 and most of
; page $06 to page $03 (but don't
; overwrite the vectors at the end of
; page $03 like the reset vector)
24A3-   A0 00       LDY   #$00
24A5-   B9 00 05    LDA   $0500,Y
24A8-   99 00 02    STA   $0200,Y
24AB-   C0 F0       CPY   #$F0
24AD-   B0 06       BCS   $24B5
24AF-   B9 00 06    LDA   $0600,Y
24B2-   99 00 03    STA   $0300,Y
24B5-   C8          INY
24B6-   D0 ED       BNE   $24A5

; set reset vector again, to some of
; the code we just copied to page $03
; (not shown, it's bad, don't go there)
24B8-   A9 03       LDA   #$03
24BA-   A2 20       LDX   #$20
24BC-   20 A3 03    JSR   $03A3

; initialize IO vectors and other stuff
24BF-   A2 26       LDX   #$26
24C1-   86 36       STX   $36
24C3-   85 37       STA   $37
24C5-   86 38       STX   $38
24C7-   85 39       STA   $39
24C9-   8E F0 03    STX   $03F0
24CC-   8D F1 03    STA   $03F1

; wipe $0C00..$BFFF with #$FF, then
; again with #$00 (not shown)
24CF-   A2 FF       LDX   #$FF
24D1-   20 E2 04    JSR   $04E2
24D4-   20 E0 04    JSR   $04E0

; and continue elsewhere
24D7-   AD 85 01    LDA   $0185
24DA-   29 5F       AND   #$5F
24DC-   AA          TAX
24DD-   4C 1D 06    JMP   $061D

I'm gonna go out on a limb here and say
that the value that ends up in X will
eventually be important, so let's
calculate it now.

*2185

2185- AD

#$AD AND #$5F = #$0D

Onward.

*261DL

261D-   4C FC 06    JMP   $06FC

Onward.

*26FCL

26FC-   CA          DEX
26FD-   CA          DEX
26FE-   9A          TXS
26FF-   60          RTS

More stack pointer fun(*), of course.

(*) not guaranteed, actual fun may vary

X was set to #$0D (at $04DC), so the
stack pointer becomes #$0B, meaning we
can find the "return" address at $010C,
which is in memory at $210C.

*210C.210D

210C- 3F 04

So execution continues at $0440.

                   ~

               Chapter 6
 In Which I'm So Tired And We Haven't
 Even Started Loading The Actual Game


$0440 is in memory at $2440.

*2440L

; save boot slot
2440-   A6 2B       LDX   $2B
2442-   8E 02 02    STX   $0202

; clear hi-res page 1
2445-   A2 20       LDX   #$20
2447-   A9 00       LDA   #$00
2449-   8D 00 20    STA   $2000
244C-   EE 4A 04    INC   $044A
244F-   D0 F8       BNE   $2449
2451-   EE 4B 04    INC   $044B
2454-   CA          DEX
2455-   D0 F2       BNE   $2449

; now show hi-res page 1
2457-   AD 52 C0    LDA   $C052
245A-   AD 57 C0    LDA   $C057
245D-   AD 54 C0    LDA   $C054
2460-   AD 50 C0    LDA   $C050
2463-   EA          NOP
2464-   EA          NOP
2465-   EA          NOP
2466-   EA          NOP

; X = 0 at this point
2467-   86 F4       STX   $F4

; probably an address
2469-   A9 20       LDA   #$20
246B-   48          PHA

; advance drive head to next track
; (A contains number of phases, and
; each phase is half a track)
246C-   A9 02       LDA   #$02
246E-   20 F0 07    JSR   $07F0

; get that address again, and pass it
; in to the multi-sector read routine
2471-   68          PLA
2472-   48          PHA

; X = number of sectors
2473-   A2 08       LDX   #$08

; read from this track, into the pages
; starting at A (so $2000 and up)
2475-   20 00 07    JSR   $0700
2478-   68          PLA
2479-   18          CLC
247A-   6D 74 04    ADC   $0474

; loop until we've loaded up to $6000
247D-   C9 60       CMP   #$60
247F-   90 EA       BCC   $246B

; advance to next track
2481-   A9 02       LDA   #$02
2483-   20 F0 07    JSR   $07F0

; read 4 sectors into $6000..$63FF
2486-   A9 60       LDA   #$60
2488-   A2 04       LDX   #$04
248A-   20 00 07    JSR   $0700

; a cute way to jump to $0400 (7-3=4)
248D-   CE 8C 04    DEC   $048C
2490-   CE 8C 04    DEC   $048C
2493-   CE 8C 04    DEC   $048C
2496-   A2 00       LDX   #$00
2498-   6C 8B 04    JMP   ($048B)

We're loading the title screen (and
showing it while it loads, because hi-
res page 1 is active), then we continue
to load more at $4000..$63FF, then we
jump to $0400.

$0400 is in memory at $2400, but first
let's capture the title screen and
whatever else is at $2000..$63FF.

*9600<C600.C6FFM

; ...code from previous traces omitted
;
; disable memory wipe (otherwise this
; trace program will go away before
; we're done with it)
9740-   A9 60       LDA   #$60
9742-   8D E2 04    STA   $04E2

; set up callback #4 after we read the
; title screen and other code into
; $2000..$63FF
9745-   A9 4C       LDA   #$4C
9747-   8D 98 04    STA   $0498
974A-   A9 58       LDA   #$58
974C-   8D 99 04    STA   $0499
974F-   A9 97       LDA   #$97
9751-   8D 9A 04    STA   $049A

; simulate the code at $06FC that set
; the stack pointer to continue at
; $0440, then "return" to it like the
; original disk does
9754-   A2 8F       LDX   #$8F
9756-   9A          TXS
9757-   60          RTS

; callback #4 --
; everything we're capturing is going
; to be preserved during a reboot, so
; just turn off the drive motor and
; reboot
9758-   AD E8 C0    LDA   $C0E8
975B-   4C 00 C5    JMP   $C500

*BSAVE TRACE4,A$9600,L$15E

*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.2000-63FF,A$2000,L$4400

Now let's see what wonders await us at
$0400.

                   ~

               Chapter 7
          Eggnorance Is Bliss


]BLOAD OBJ.0400-07FF,A$2400
]CALL -151

*2400L

; pull values off the stack and
; accumulate them in zero page $48
2400-   68          PLA
2401-   18          CLC
2402-   65 48       ADC   $48
2404-   85 48       STA   $48
2406-   CA          DEX
2407-   D0 F7       BNE   $2400

; seek to track $0A
2409-   A9 14       LDA   #$14
240B-   20 72 07    JSR   $0772

; read 1 sector into $0500
240E-   A2 01       LDX   #$01
2410-   A9 05       LDA   #$05
2412-   20 00 07    JSR   $0700

; seek to track $0B
2415-   A9 16       LDA   #$16
2417-   20 72 07    JSR   $0772

; use the checksum in zero page $48
; to calculate the stack pointer,
; because why did you expect anything
; to be easy?
241A-   A5 48       LDA   $48
241C-   4D 95 01    EOR   $0195
241F-   AA          TAX
2420-   9A          TXS

; then jump to... somewhere
2421-   6C 00 02    JMP   ($0200)

At this point, $0200..$02FF is a copy
of $0500..$05FF (set at $04A3), so the
target of the indirect jump at $0421 is
in memory at $2500. Hooray!

*2500.2501

2500- 00 63

And that's part of the code we just
captured, along with the title screen.
Hooray!

*BLOAD OBJ.2000-63FF,A$2000

*6300L

6300-   20 66 63    JSR   $6366

*6366L

6366-   A0 07       LDY   #$07
6368-   20 3B 63    JSR   $633B
636B-   90 06       BCC   $6373
636D-   BD 81 C0    LDA   $C081,X
6370-   BD 81 C0    LDA   $C081,X
6373-   88          DEY
6374-   10 F2       BPL   $6368
6376-   60          RTS

*633BL

; This is a slot scan. It takes a slot
; number (in Y, iterated in the caller)
; and copies the peripheral ROM page
; at $Cy00 to $B000.
633B-   98          TYA
633C-   48          PHA
633D-   A2 00       LDX   #$00
633F-   09 C0       ORA   #$C0
6341-   86 11       STX   $11
6343-   85 12       STA   $12
6345-   0A          ASL
6346-   0A          ASL
6347-   0A          ASL
6348-   0A          ASL
6349-   AA          TAX
634A-   F0 16       BEQ   $6362
634C-   A0 00       LDY   #$00
634E-   B1 11       LDA   ($11),Y
6350-   99 00 B0    STA   $B000,Y
6353-   C8          INY
6354-   D0 F8       BNE   $634E

; Then check if the peripheral ROM page
; has changed, and set the carry flag
; if so.
6356-   B9 00 B0    LDA   $B000,Y
6359-   D1 11       CMP   ($11),Y
635B-   D0 05       BNE   $6362
635D-   C8          INY
635E-   D0 F6       BNE   $6356
6360-   18          CLC
6361-   24 38       BIT   $38
6363-   68          PLA
6364-   A8          TAY
6365-   60          RTS

In the caller, it checks the carry flag
and, if the peripheral ROM changed
during this operation, it hits a
softswitch based on the slot number
(x16).

636B-   90 06       BCC   $6373
636D-   BD 81 C0    LDA   $C081,X
6370-   BD 81 C0    LDA   $C081,X

I don't know the purpose of this check.
Given the general unfriendliness of,
well, everything so far, I'm going to
assume it's trying to disable some
problematic (read: hacker-friendly)
peripheral card. Reading $C081 (not
indexed) would switch the language card
RAM bank to read from ROM while writing
to RAM bank 2, which is the same as the
configuration we set earlier. Perhaps
there were slotted RAM cards that
offered similar overrides? And we're
trying extra hard to ensure that they
are also set to the same "read from
ROM, write to RAM" configuration?

Aside from hitting $C081,X twice (where
X = slot number x16), there are no side
effects to this routine. Onward.

Continuing from $6303...

; Copy F8 ROM into language card (if
; RAM bank 2 is writeable, and we have
; certainly taken every precaution to
; ensure that it is)
6303-   A0 00       LDY   #$00
6305-   A9 F8       LDA   #$F8
6307-   84 11       STY   $11
6309-   85 12       STA   $12
630B-   B1 11       LDA   ($11),Y
630D-   91 11       STA   ($11),Y
630F-   C8          INY
6310-   D0 F9       BNE   $630B
6312-   E6 12       INC   $12
6314-   D0 F5       BNE   $630B

; once again call the entire slot scan
6316-   20 66 63    JSR   $6366

; and now this
6319-   20 A0 62    JSR   $62A0

*62A0L

62A0-   A0 00       LDY   #$00
62A2-   A2 00       LDX   #$00
62A4-   86 00       STX   $00

; look at bytes at $6200
62A6-   B9 00 62    LDA   $6200,Y

; if we find a #$00 byte, fail
62A9-   F0 4A       BEQ   $62F5

; skip if we've already set the high
; bit of zero page $00
62AB-   24 00       BIT   $00
62AD-   30 08       BMI   $62B7

; otherwise compare to bytes in ROM
62AF-   DD FA FF    CMP   $FFFA,X
62B2-   F0 03       BEQ   $62B7

; if any byte doesn't match, set the
; high bit of zero page $00
62B4-   38          SEC
62B5-   66 00       ROR   $00
62B7-   C8          INY
62B8-   E8          INX

; compare up to 6 bytes
62B9-   E0 06       CPX   #$06
62BB-   90 E9       BCC   $62A6

; if any byte didn't match, start over
; with a different Y offset
62BD-   C8          INY
62BE-   C8          INY
62BF-   24 00       BIT   $00
62C1-   30 DF       BMI   $62A2
62C3-   60          RTS

The failure path (at $62A9) jumps to
$62F5, which jumps to a routine at
$032F, which we copied there earlier.
It clears memory, displays an "M" error
code, and reboots.

The success path will make more sense
if we look at the bytes at $6000+.

*6000.

6200- 62 FA 62 FA 40 FA 00 00
6208- FB 03 59 FF 86 FA 00 00
6210- FB 03 62 FA 40 FA 00 00
6218- 00

We're checking the last six bytes of
ROM ($FFFA..$FFFF) against these
signatures -- the set starting at
$6200, $6208, or $6210. It doesn't
matter which signature matches, but all
six bytes must match or else we move to
the next one (and eventually give up
and reboot with an "M" error code).

Thanks to modern emulators, I have a
wide array of ROMs at my fingertips.

$6200: Apple /// (in II emulation mode)
$6208: Apple ][
$6210: Apple ][+
       Apple II j-plus
       Apple II Europlus
       Apple //e (unenhanced)

None of these signatures match my
enhanced Apple //e or any later model,
which explains why my original disk
refuses to boot on my real Apple II
with genuine Apple ROMs.

So that's great.

Continuing from $631C (only if the ROM
check has succeeded)...

; try to restore the softswitches we
; twiddled earlier on any slotted RAM
; cards or whatever the heck these are
631C-   A0 07       LDY   #$07
631E-   84 10       STY   $10
6320-   20 3B 63    JSR   $633B
6323-   90 0D       BCC   $6332
6325-   86 13       STX   $13
6327-   BD 80 C0    LDA   $C080,X
632A-   20 A0 62    JSR   $62A0
632D-   A6 13       LDX   $13
632F-   BD 81 C0    LDA   $C081,X
6332-   A4 10       LDY   $10
6334-   88          DEY
6335-   10 E7       BPL   $631E

; switch back to reading RAM bank 2
6337-   AD 80 C0    LDA   $C080

; and... exit to the caller
633A-   60          RTS

Of course, "the caller" isn't "where we
came from," because the last thing we
did before jumping to $6300 was reset
the stack pointer (at $0420). Reset to
what? I don't know, but it was based on
zero page $48, which was based on a
checksum of the entire stack page,
which means we get to restore the 3
bytes of the stack that we modified in
the previous trace so we can figure out
where we're "returning" to.

                   ~

               Chapter 8
         Obfuscation Most Fowl


*9600<C600.C6FFM

; ...code from previous traces omitted
;
; disable memory wipe (otherwise this
; trace program will go away before
; we're done with it)
9740-   A9 60       LDA   #$60
9742-   8D E2 04    STA   $04E2

; set up callback #4 after we read the
; title screen and other code into
; $2000..$63FF
9745-   A9 4C       LDA   #$4C
9747-   8D 98 04    STA   $0498
974A-   A9 66       LDA   #$66
974C-   8D 99 04    STA   $0499
974F-   A9 97       LDA   #$97
9751-   8D 9A 04    STA   $049A

; restore bytes on the stack that we
; patched earlier
9754-   A9 AE       LDA   #$AE
9756-   8D FB 01    STA   $01FB
9759-   A9 65       LDA   #$65
975B-   8D FC 01    STA   $01FC
975E-   A9 05       LDA   #$05
9760-   8D FD 01    STA   $01FD

; continue the boot
9763-   4C FB 01    JMP   $01FB

; callback #4 --
; set up callback #5 after we checksum
; the stack page and calculate the new
; stack pointer
9766-   A9 4C       LDA   #$4C
9768-   8D 21 04    STA   $0421
976B-   A9 78       LDA   #$78
976D-   8D 22 04    STA   $0422
9770-   A9 97       LDA   #$97
9772-   8D 23 04    STA   $0423

; continue the boot
9775-   4C 00 04    JMP   $0400

; callback #5 --
; copy the new sector we read into
; $0500 (at $0412)
9778-   A0 00       LDY   #$00
977A-   B9 00 05    LDA   $0500,Y
977D-   99 00 25    STA   $2500,Y
9780-   C8          INY
9781-   D0 F7       BNE   $977A

; save the calculated stack pointer
; (still in X at this point)
9783-   8E 8C 97    STX   $978C

; turn off drive motor and reboot to
; our work disk
9786-   AD E8 C0    LDA   $C0E8
9789-   4C 00 C5    JMP   $C500

*BSAVE TRACE5,A$9600,L$18C
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0500-05FF,A$2500,L$100

Now let's see where the stack ends up.

]CALL -151

*978C

978C- FB

*BLOAD OBJ.0100-01FF,A$2100

*21FC.21FD

21FC- 65 05

Hilarious side note: this is real code,
because of course it is. In fact, it's
2/3 of the "LDX $0565" instruction that
we just restored, except now it points
to the new sector we just read into
$0500, which we just captured and saved
and is in memory at $2500.

So here we go.

*2566L

; wipe the stack by pushing 256 zeroes
2566-   A2 00       LDX   #$00
2568-   8A          TXA
2569-   48          PHA
256A-   CA          DEX
256B-   D0 FC       BNE   $2569

; reset the stack pointer
256D-   A2 FF       LDX   #$FF
256F-   9A          TXS

; continue elsewhere
2570-   4C 50 05    JMP   $0550

*2550L

; set reset vector (not shown)
2550-   A2 20       LDX   #$20
2552-   A9 03       LDA   #$03
2554-   20 A3 03    JSR   $03A3

; continue elsewhere
2557-   A6 2B       LDX   $2B
2559-   4C 00 05    JMP   $0500

*2500L

2500-   A9 FF       LDA   #$FF
2502-   20 37 05    JSR   $0537
2505-   C9 DD       CMP   #$DD
2507-   D0 F7       BNE   $2500
2509-   20 39 05    JSR   $0539
250C-   C9 F5       CMP   #$F5
250E-   D0 F5       BNE   $2505
2510-   20 39 05    JSR   $0539
2513-   C9 D5       CMP   #$D5
2515-   D0 F5       BNE   $250C
2517-   20 30 05    JSR   $0530

*2530L

; main entry point will read 2 nibbles
; from disk and create one byte out of
; them (standard "4-and-4" encoding)
2530-   BD 8C C0    LDA   $C08C,X
2533-   10 FB       BPL   $2530
2535-   38          SEC
2536-   2A          ROL

; entry point here (used for first
; nibble, at $0502)
2537-   85 FF       STA   $FF

; entry point here (used for subsequent
; nibbles at $0509 and $0510)
2539-   BD 8C C0    LDA   $C08C,X
253C-   10 FB       BPL   $2539
253E-   25 FF       AND   $FF
2540-   60          RTS

Fine, so we're looking for a custom
prologue. No problem.

Continuing from $051A...

; store the byte later in this very
; routine
251A-   8D 27 05    STA   $0527

; get another byte and store that one
251D-   20 30 05    JSR   $0530
2520-   8D 28 05    STA   $0528

; get another byte and store that in
; the address pointed to by the first
; two bytes
2523-   20 30 05    JSR   $0530
2526-   8D FF FF    STA   $FFFF

; if the third byte of the triplet was
; #$EA, I guess we're done
2529-   C9 EA       CMP   #$EA

; otherwise branch back and do it again
252B-   D0 EA       BNE   $2517

; once we're done, continue elsewhere
252D-   4C 41 05    JMP   $0541

*2541L

; turn off drive motor and... reboot?!?
2541-   BD 88 C0    LDA   $C088,X
2544-   6C F2 03    JMP   ($03F2)

Well wait a minute. Before we solve the
mystery of "how does this game boot,"
we have a bigger problem. This is a
self-modifying loop which reads
addresses and values from disk and
modifies memory. I have no idea which
addresses are modified or what values
they take on. I have no idea how many
addresses are modified. I can't trust
any code listing at all because this
loop could literally change anything
anywhere in memory.

So that's great.

                   ~

               Chapter 9
         In Which No Good Deed
           Goose Unpunished


To find out what effects this loop has,
we can duplicate this entire routine
and modify it to construct an array of
addresses and values instead of setting
the addresses themselves.

*9600<C600.C6FFM

; ...code from previous traces omitted
;
; callback #5 --
; duplicate the logic of the loop at
; $0500
9778-   A6 2B       LDX   $2B
977A-   A9 FF       LDA   #$FF
977C-   20 37 05    JSR   $0537
977F-   C9 DD       CMP   #$DD
9781-   D0 F7       BNE   $977A
9783-   20 39 05    JSR   $0539
9786-   C9 F5       CMP   #$F5
9788-   D0 F5       BNE   $977F
978A-   20 39 05    JSR   $0539
978D-   C9 D5       CMP   #$D5
978F-   D0 F5       BNE   $9786
9791-   A0 00       LDY   #$00
9793-   20 30 05    JSR   $0530

; but save each address and value in a
; table at $8000 (safe, unused)
9796-   99 00 80    STA   $8000,Y
9799-   C8          INY
979A-   20 30 05    JSR   $0530
979D-   99 00 80    STA   $8000,Y
97A0-   C8          INY
97A1-   20 30 05    JSR   $0530
97A4-   99 00 80    STA   $8000,Y

; I don't know how many addresses and
; values we're getting, but I hope it's
; fewer than 256/3
97A7-   C8          INY
97A8-   C9 EA       CMP   #$EA
97AA-   D0 E7       BNE   $9793

; turn off drive motor and reboot to
; the work disk
97AC-   AD E8 C0    LDA   $C0E8
97AF-   4C 00 C5    JMP   $C500

*BSAVE TRACE6,A$9600,L$1B2

; fill memory with an unusual byte to
; make sure that the values I'm seeing
; later at $8000 are the actual values
; being read from the disk (what, you
; thought all these trace programs
; worked on the first try?)
*800:FD N 801<800.BEFEM

*BRUN TRACE6
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.8000-80FF,A$8000,L$100
]CALL -151

*8000.

8000- 20 00 D5 21 00 AA 22 00
8008- D4 23 00 B5 16 07 20 5B
8010- 07 20 65 07 20 1D 07 21
8018- 56 07 21 24 07 22 51 07
8020- 22 44 07 23 60 07 23 44
8028- 05 4C 45 05 00 46 05 40
8030- 70 05 E8 71 05 E8 72 05
8038- E8 41 01 65 42 01 05 00
8040- 00 EA FD FD FD FD FD FD

Here is the same data, reformatted for
readability:

 address | value
---------+-------
  $0020  |  $D5
  $0021  |  $AA
  $0022  |  $D4
  $0023  |  $B5
  $0716  |  $20
  $075B  |  $20
  $0765  |  $20
  $071D  |  $21
  $0756  |  $21
  $0724  |  $22
  $0751  |  $22
  $0744  |  $23
  $0760  |  $23
  $0544  |  $4C
  $0545  |  $00
  $0546  |  $40
  $0570  |  $E8
  $0571  |  $E8
  $0572  |  $E8
  $0141  |  $65
  $0142  |  $05
  $0000  |  $EA (exit condition)

The changes to zero page and the $0700
range are about changing the RWTS
prologue and the addresses from which
to compare them. We'll jump off that
bridge when we come to it.

More interestingly and immediately,
$0544 becomes a "JMP $4000" instruction
which explains how this game loads. The
code I saw at $0544 is not the code
that is at $0544 when we get around to
executing the code at $0544. This loop
self-modifies its own exit condition by
reading values from disk. Sneaky.

Even better, $0570 becomes three "INX"
instructions, which wipes out the
"JMP $0500" instruction we executed the
last time we were at $0570. That means
the routine at $0566 will fall through
if it's ever called again. I'm guessing
that will happen soon enough.

$0141/$0142 (on the stack) become a
pointer to $0565, so if we were to set
the stack pointer to #$40 and "RTS" to
return to the caller, we would end up
at the newly modified routine at $0566.
I'm guessing that will happen soon too.

Bottom line: after this self-modifying
loop exits, execution continues at
$4000, which we captured earlier.

*BLOAD OBJ.2000-63FF,A$2000

*4000L

4000-   20 00 4C    JSR   $4C00
4003-   A2 40       LDX   #$40
4005-   9A          TXS
4006-   60          RTS

*4C00G
[...plays title screen sound and
 returns to monitor...]

(FUN FACT: according to the developer,
this protection was designed to be a
reusable "turnkey" protection scheme,
to be licensed to different publishers
(Broderbund Software and Gebelli
Software used it) and used in multiple
games. $4000 was the entry point for
the game developer to do something fun
once the title screen had loaded. The
way to return control to the bootloader
was to set the stack pointer to #$40
and RTS. Each game that shares this
protection has different code at $4000.
Some, like this one, have animation and
sound. Some just wait for a keypress.
Some do nothing at all.)

                   ~

              Chapter 10
         Release The Crackin'


$4003 sets the stack pointer to #$40
and issues an "RTS", so execution
continues at the (NOTE: newly modified)
routine at $0566. We captured the pre-
modified version of that, and now that
we've captured all the changes made by
the loop at $0500, we can apply them
and see what will really happen.

*BLOAD OBJ.0500-05FF,A$2500

*2570:E8 E8 E8

*2566L

; clear the stack (again)
2566-   A2 00       LDX   #$00
2568-   8A          TXA
2569-   48          PHA
256A-   CA          DEX
256B-   D0 FC       BNE   $2569

; reset the stack pointer (again)
256D-   A2 FF       LDX   #$FF
256F-   9A          TXS

; since this was modified earlier (in
; the self-modifying loop at $0500),
; we fall through here and continue
; instead of jumping back to $0550
2570-   E8          INX
2571-   E8          INX
2572-   E8          INX

And just like that, we fall through to
an entirely different routine the
second time around.

; turn on the drive motor again
2573-   A6 2B       LDX   $2B
2575-   BD 89 C0    LDA   $C089,X

; wait for drive to spin up
2578-   A0 80       LDY   #$80
257A-   CA          DEX
257B-   D0 FD       BNE   $257A
257D-   88          DEY
257E-   D0 FA       BNE   $257A

; seek to track $0B + 1 phase, so I
; guess you could call it track $0B.5
2580-   A9 17       LDA   #$17
2582-   20 72 07    JSR   $0772

; maybe a counter? or high byte of an
; address?
2585-   A9 08       LDA   #$08
2587-   48          PHA

; move drive head forward one phase
; (half track), so now we're on track
; $0C
2588-   18          CLC
2589-   A5 F4       LDA   $F4
258B-   69 01       ADC   #$01
258D-   20 72 07    JSR   $0772

; read 4 sectors into the address in A,
; starting at $0800 (set at $0585,
; which was an address after all)
2590-   A2 04       LDX   #$04
2592-   68          PLA
2593-   48          PHA
2594-   20 00 07    JSR   $0700
2597-   68          PLA
2598-   18          CLC
2599-   69 04       ADC   #$04

; once we read $0800..$1FFF, skip the
; hi-res page which is presumably still
; showing the title screen
259B-   C9 20       CMP   #$20
259D-   90 E8       BCC   $2587

; if the previous CMP value is already
; greater than #$80, we're done
259F-   2C 9C 05    BIT   $059C
25A2-   30 0C       BMI   $25B0

; ah, here's why: that CMP value is
; modified during the loop -- after we
; skip hi-res page 1 ($2000..$3FFF) and
; continue reading into $4000, we set
; the ending address to $C000
25A4-   A9 40       LDA   #$40
25A6-   8D 86 05    STA   $0586
25A9-   A9 C0       LDA   #$C0
25AB-   8D 9C 05    STA   $059C

; unconditionally branch back to
; continue reading into $4000..$BFFF
25AE-   30 D5       BMI   $2585

; execution continues here from the BMI
; at $05A2, by which point we've filled
; all of main memory (minus the hi-res
; page) by reading 4 sectors each from
; [...counts on fingers and toes...]
; [...runs out of both...]
; 40 consecutive half tracks (!!!)
;
; now seek to track $20
25B0-   A9 40       LDA   #$40
25B2-   20 72 07    JSR   $0772

; read 1 sector from track $20, into
; $0600
25B5-   A2 01       LDX   #$01
25B7-   A9 06       LDA   #$06
25B9-   20 00 07    JSR   $0700

; and jump to the final sector we just
; read from track $20
25BC-   A9 05       LDA   #$05
25BE-   48          PHA
25BF-   A9 FF       LDA   #$FF
25C1-   48          PHA
25C2-   60          RTS

This is really the heart of the copy
protection scheme, from the point of
view of "making the disk impossible to
copy by classic bit copy programs." All
the obfuscation is nice and clever and
makes my life difficult (hi Roland!)
but will only ever be seen by a handful
of people. The vast, vast majority of
people who try to "defeat" the copy
protection will only try to make a full
disk backup with the protection intact.
That means the structural protection --
in full bloom here -- is paramount.

What's so difficult about this disk
structure? It's that the data is stored
on consecutive half tracks, in a spiral
pattern. The entire reason that tracks
on a disk are spaced apart from each
other is so you can store data all the
way around the track. The drive head is
wide enough that, when you write data
on one track, it actually contaminates
the adjacent space on either side with
a partial copy of the data. That's why
you can't just double the available
disk space on a DOS 3.3 formatted disk
by storing data on half and whole
tracks. Every time you wrote to a whole
track, it would overwrite some of the
data on the adjacent half tracks.

But Roland didn't use DOS 3.3's naive
write routines, and it doesn't store an
entire track's worth of data on each
track. It's only reading four 4-and-4-
encoded sectors before moving the drive
head to the next half track. Those four
sectors only take up about 1/3 of the
track. Even taking into account the
time spent moving from one half track
to the next, the custom writer program
that Roland created to write this data
to the original disk would never
overwrite real data stored on previous
half tracks.

Bit copiers, on the other hand, have no
specific knowledge of how this data was
written so cleverly. They don't have
access to Roland's original mastering
program. All they know is that there is
data on track $0C, data on track $0C.5,
data on track $0D, &c.

What to do? For bit copiers, the answer
was to fake it with quarter tracks. For
example, here is the relevant portion
of Locksmith's "parameter file" to copy
a disk with this protection scheme:

SYNC YES:COPY C.25 1E.25 1

Briefly, "SYNC YES" means "attempt to
synchronize the tracks we write out,
even though the Disk II doesn't have
any built-in way to do that." This sort
of kind of worked, but it was dependent
on your drive speed being accurate and
tended to work best on lower tracks
(which these are not).

"C.25 1E.25 1" means "read from, and
write to, track $0C.25, $0D.25, $0E.25,
up to $1E.25, and rely on the fact that
the drive head wrote the data to the
adjacent space on either side when it
was originally written, so if we read
in the middle we might capture all the
data on track $0C and $0C.5 in one
shot." This worked some of the time on
some drives, some better than others.
But it was the best they could do, so
that's what they did.

(Amusing side note: these "fake it with
quarter tracks" parameter files led to
a vocal contigent of people claming
that quarter tracks weren't real, i.e.
they were a figment invented by bit
copiers to hide their shortcomings and
no one could actually store data on
quarter tracks.)

At any rate, we're not using a bit
copier, we don't want a protected
backup, and we're up to [counts on
fingers and toes] seven (!)  callbacks
in our trace program to control the
boot process before we can finally
capture the remaining data in memory.

*9600<C600.C6FFM

; ...code from previous traces omitted
;
; callback #5 --
; set up callback #6 after the self-
; modifying loop finishes modifying
; itself (but, luckily, not these three
; bytes)
9778-   A9 4C       LDA   #$4C
977A-   8D 41 05    STA   $0541
977D-   A9 88       LDA   #$88
977F-   8D 42 05    STA   $0542
9782-   A9 97       LDA   #$97
9784-   8D 43 05    STA   $0543

; continue the boot
9787-   60          RTS

; callback #6 --
; change target address of sector we
; read into $0600 to read into $2600
; instead
9788-   A9 26       LDA   #$26
978A-   8D B8 05    STA   $05B8

; change address pushed to the stack
; so execution continues at $2000
; instead
978D-   A9 1F       LDA   #$1F
978F-   8D BD 05    STA   $05BD

; copy callback #7 to $2000
9792-   A2 00       LDX   #$00
9794-   BD A1 97    LDA   $97A1,X
9797-   9D 00 20    STA   $2000,X
979A-   E8          INX
979B-   D0 F7       BNE   $9794

; continue the boot
979D-   A2 40       LDX   #$40
979F-   9A          TXS
97A0-   60          RTS

; callback #7 (executed at $2000) --
; copy the two pages that will get
; overwritten when we reboot to the
; work disk
97A1-   A0 00       LDY   #$00
97A3-   B9 00 08    LDA   $0800,Y
97A6-   99 00 28    STA   $2800,Y
97A9-   B9 00 BF    LDA   $BF00,Y
97AC-   99 00 2F    STA   $2F00,Y
97AF-   C8          INY
97B0-   D0 F1       BNE   $97A3

; turn off the drive motor and reboot
; to the work disk
97B2-   AD E8 C0    LDA   $C0E8
97B5-   4C 00 C5    JMP   $C500

*BSAVE TRACE7,A$9600,L$1B8

*800:FD N 801<800.BEFEM

*BRUN TRACE7
...reboots slot 6...
...reboots slot 5...

; page 8 is at $2800
]BSAVE OBJ2.0800-08FF,A$2800,L$100

; page 6 is at $2600
]BSAVE OBJ2.0600-06FF,A$2600,L$100

; page $BF is at $2F00
]BSAVE OBJ2.BF00-BFFF,A$2F00,L$100

; everything else is where it is
]BSAVE OBJ2.0900-1FFF,A$900,L$1700
]BSAVE OBJ2.4000-BEFF,A$4000,L$7F00

                   ~

              Chapter 11
    Obfuscation Most Fowl Part II:
      The Fowl And The Furry-ous


]CALL -151

*2600L

2600-   20 BF 06    JSR   $06BF

*26BFL

26BF-   A9 FF       LDA   #$FF
26C1-   20 F6 06    JSR   $06F6
26C4-   C9 D5       CMP   #$D5
26C6-   D0 F7       BNE   $26BF
26C8-   20 F8 06    JSR   $06F8
26CB-   C9 FF       CMP   #$FF
26CD-   D0 F5       BNE   $26C4
26CF-   20 F8 06    JSR   $06F8
26D2-   C9 D5       CMP   #$D5
26D4-   D0 F5       BNE   $26CB
26D6-   20 EF 06    JSR   $06EF
26D9-   8D E6 06    STA   $06E6
26DC-   20 EF 06    JSR   $06EF
26DF-   8D E7 06    STA   $06E7
26E2-   20 EF 06    JSR   $06EF
26E5-   8D FF FF    STA   $FFFF
26E8-   C9 EA       CMP   #$EA
26EA-   D0 EA       BNE   $26D6
26EC-   4C FF 06    JMP   $06FF
26EF-   BD 8C C0    LDA   $C08C,X
26F2-   10 FB       BPL   $26EF
26F4-   38          SEC
26F5-   2A          ROL
26F6-   85 FF       STA   $FF
26F8-   BD 8C C0    LDA   $C08C,X
26FB-   10 FB       BPL   $26F8
26FD-   25 FF       AND   $FF
26FF-   60          RTS

YOU HAVE GOT TO BE KIDDING ME.

[narrator] He was not kidding you.

This is another self-modifying loop
that reads arbitrary <address:value>
pairs from disk and sets them, just
like the one we saw earlier at $0500.
It even has the same exit condition:
reading an #$EA value.

Hi ho, hi ho, it's back to trace we go.

*9600<C600.C6FFM

; ...code from previous traces omitted
;
; callback #6 --
; change address pushed to the stack
; so execution continues at $2000
; instead
9788-   A9 1F       LDA   #$1F
978A-   8D BD 05    STA   $05BD

; copy callback #7 to $2000
978D-   A2 00       LDX   #$00
978F-   BD 9C 97    LDA   $979C,X
9792-   9D 00 20    STA   $2000,X
9795-   E8          INX
9796-   D0 F7       BNE   $978F

; continue the boot
9798-   A2 40       LDX   #$40
979A-   9A          TXS
979B-   60          RTS

; callback #7 (executed at $2000) --
; reproduce the self-modifying loop at
; $06BF but store both addresses and
; values in our own table instead of
; setting them
979C-   A9 FF       LDA   #$FF
979E-   20 F6 06    JSR   $06F6
97A1-   C9 D5       CMP   #$D5
97A3-   D0 F7       BNE   $979C
97A5-   20 F8 06    JSR   $06F8
97A8-   C9 FF       CMP   #$FF
97AA-   D0 F5       BNE   $97A1
97AC-   20 F8 06    JSR   $06F8
97AF-   C9 D5       CMP   #$D5
97B1-   D0 F5       BNE   $97A8

; read addresses and values and store
; them at $8000+
97B3-   A0 00       LDY   #$00
97B5-   20 EF 06    JSR   $06EF
97B8-   99 00 80    STA   $8000,Y
97BB-   C8          INY
97BC-   20 EF 06    JSR   $06EF
97BF-   99 00 80    STA   $8000,Y
97C2-   C8          INY
97C3-   20 EF 06    JSR   $06EF
97C6-   99 00 80    STA   $8000,Y
97C9-   C8          INY

; same exit condition
97CA-   C9 EA       CMP   #$EA
97CC-   D0 E7       BNE   $97B5

; turn off drive motor and reboot to
; the work disk
97CE-   AD E8 C0    LDA   $C0E8
97D1-   4C 00 C5    JMP   $C500

*BSAVE TRACE8,A$9600,L$1D4
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ2.8000-80FF,A$8000,L$100
]CALL -151

*8000.

8000- 20 00 80 21 00 9F A8 FC
8008- 38 4E 00 00 4F 00 1F 36
8010- 00 0B 37 00 1F 38 00 D0
8018- 39 00 51 00 02 50 01 02
8020- 18 3A 00 00 3B 00 59 EA
8028- EA EA 00 00 00 7C 66 66

Here is the same data, reformatted for
readability:

 address | value
---------+-------
  $0020  |  $80
  $0021  |  $9F
  $FCA8  |  $38
  $004E  |  $00
  $004F  |  $1F
  $0036  |  $0B
  $0037  |  $1F
  $0038  |  $D0
  $0039  |  $51
  $0200  |  $50
  $0201  |  $18
  $003A  |  $00
  $003B  |  $59
  $EAEA  |  $EA (exit condition)

I assume all of these changes are
required and will be checked in subtle
and non-obvious ways and WAIT A MINUTE
WHAT THE HECK ARE WE DOING TO $FCA8?

To state the obvious, $FCA8 is in ROM.
Slightly less obvious: it's the entry
point to a commonly used routine that
waits for a certain number of CPU
cycles. By now, we've copied the F8 ROM
into RAM bank 2, so I guess it's
technically writeable. But why write it
now, and why write this value?

Checking ROM, $FCA8 is already supposed
to be #$38 (it's a "SEC" opcode). So...
we're setting our copy of $FCA8 to...
itself?

In fact, we're setting it back to its
original value, because (hold on to
your hats) THE TITLE SCREEN SNEAKILY
SET IT TO ZERO while I wasn't looking.

Take a look:

*BLOAD OBJ.2000-63FF,A$2000
*4000L

4000-   20 00 4C    JSR   $4C00

*4C00L

4C00-   A9 00       LDA   #$00
4C02-   85 0A       STA   $0A
4C04-   20 00 4D    JSR   $4D00
4C07-   20 40 4C    JSR   $4C40
4C0A-   A9 01       LDA   #$01
4C0C-   20 A8 FC    JSR   $FCA8
4C0F-   A5 0A       LDA   $0A
4C11-   D0 F1       BNE   $4C04
4C13-   A9 20       LDA   #$20
4C15-   85 0A       STA   $0A
4C17-   20 90 4C    JSR   $4C90
4C1A-   C6 0A       DEC   $0A
4C1C-   D0 F9       BNE   $4C17
4C1E-   A9 10       LDA   #$10
4C20-   85 0A       STA   $0A
4C22-   20 B0 4C    JSR   $4CB0
4C25-   C6 0A       DEC   $0A
4C27-   D0 F9       BNE   $4C22
4C29-   60          RTS

*4CB0L

4CB0-   A9 00       LDA   #$00
4CB2-   85 00       STA   $00
4CB4-   A9 20       LDA   #$20
4CB6-   85 01       STA   $01
4CB8-   A0 00       LDY   #$00
4CBA-   B1 00       LDA   ($00),Y
4CBC-   49 80       EOR   #$80
4CBE-   91 00       STA   ($00),Y
4CC0-   C8          INY
4CC1-   D0 F7       BNE   $4CBA
4CC3-   A9 00       LDA   #$00
4CC5-   8D A8 FC    STA   $FCA8 <-- WTF
4CC8-   E6 01       INC   $01
4CCA-   A5 01       LDA   $01
4CCC-   29 1F       AND   #$1F
4CCE-   D0 E8       BNE   $4CB8
4CD0-   60          RTS

While the title screen is flashing and
shifting colors (by toggling the high
bit of every pixel), it's also laying a
trap for unsuspecting hackers by
putting a "BRK" instruction at the
entry point of a common ROM routine,
then fixing it at the last minute while
reading a bunch of unrelated values
from disk in a self-modifying loop.

Great.

Continuing from $0603...

*BLOAD OBJ2.0600-06FF,A$2600

*2603L

; seek to track $22
2603-   A9 44       LDA   #$44
2605-   8D 04 02    STA   $0204
2608-   20 72 07    JSR   $0772

; clear text screen (except the current
; page)
260B-   A0 00       LDY   #$00
260D-   A9 A0       LDA   #$A0
260F-   99 00 04    STA   $0400,Y
2612-   99 00 05    STA   $0500,Y
2615-   99 00 07    STA   $0700,Y
2618-   C8          INY
2619-   D0 F4       BNE   $260F
261B-   EA          NOP
261C-   EA          NOP
261D-   EA          NOP
261E-   EA          NOP
261F-   EA          NOP

; death counter
2620-   A9 40       LDA   #$40
2622-   85 FE       STA   $FE
2624-   88          DEY
2625-   D0 07       BNE   $262E
2627-   C6 FE       DEC   $FE
2627-   C6 FE       DEC   $FE
2629-   D0 03       BNE   $262E
262B-   4C 6D 06    JMP   $066D

; find a 3-nibble prologue on track $22
; ($D5 $FF $DD)
262E-   BD 8C C0    LDA   $C08C,X
2631-   10 FB       BPL   $262E
2633-   C9 D5       CMP   #$D5
2635-   D0 ED       BNE   $2624
2637-   BD 8C C0    LDA   $C08C,X
263A-   10 FB       BPL   $2637
263C-   C9 FF       CMP   #$FF
263E-   D0 F3       BNE   $2633
2640-   BD 8C C0    LDA   $C08C,X
2643-   10 FB       BPL   $2640
2645-   C9 DD       CMP   #$DD
2647-   D0 F3       BNE   $263C

; read a page worth of 4-4-encoded data
2649-   A0 00       LDY   #$00
264B-   BD 8C C0    LDA   $C08C,X
264E-   10 FB       BPL   $264B
2650-   38          SEC
2651-   2A          ROL
2652-   85 FF       STA   $FF
2654-   EA          NOP
2655-   BD 8C C0    LDA   $C08C,X
2658-   10 FB       BPL   $2655
265A-   25 FF       AND   $FF

; store it in $0400 (which we just
; cleared)
265C-   99 00 04    STA   $0400,Y
265F-   C8          INY
2660-   D0 E9       BNE   $264B

; look for a 1-nibble epilogue ($D5)
2662-   BD 8C C0    LDA   $C08C,X
2665-   10 FB       BPL   $2662
2667-   C9 D5       CMP   #$D5

; branch either way
2669-   D0 02       BNE   $266D
266B-   F0 07       BEQ   $2674

; if the epilogue was not correct,
; wipe the page we just read
266D-   98          TYA
266E-   99 00 04    STA   $0400,Y
2671-   C8          INY
2672-   D0 FA       BNE   $266E

; turn off drive motor
2674-   AE 02 02    LDX   $0202
2677-   BD 88 C0    LDA   $C088,X

; initialize some zero page locations
267A-   A0 00       LDY   #$00
267C-   84 F0       STY   $F0
267E-   84 F1       STY   $F1
2680-   84 F2       STY   $F2
2682-   84 F3       STY   $F3

; checksum 18 pages starting at $0800
2684-   A2 18       LDX   #$18
2686-   A9 08       LDA   #$08
2688-   20 A6 06    JSR   $06A6

; checksum 128 pages starting at $4000
268B-   A2 80       LDX   #$80
268D-   A9 40       LDA   #$40
268F-   20 A6 06    JSR   $06A6

; verify the checksum
2692-   A5 F2       LDA   $F2
2694-   C9 43       CMP   #$43
2696-   D0 09       BNE   $26A1
2698-   A5 F3       LDA   $F3
269A-   C9 27       CMP   #$27
269C-   D0 03       BNE   $26A1

; checksum passed, start the game by
; jumping to an address that was set
; during the self-modifying loop at
; $06BF
269E-   6C 20 00    JMP   ($0020)

; checksum failed, off to The Badlands
26A1-   4C 29 03    JMP   $0329

According to the data we captured from
the self-modifying loop, ($0020) points
to $9F80.

But what's all this about reading one
sector from track $22?

Hi ho, hi ho...

                   ~

              Chapter 12
      Don't Shell Yourself Short


*9600<C600.C6FFM

; ...code from previous traces omitted
;
; callback #7 (executed at $2000) --
; change target address of final read
; from $0400 to $2400
979C-   A9 24       LDA   #$24
979E-   8D 5E 06    STA   $065E

; set up callback #8 after read is
; complete
97A1-   A9 4C       LDA   #$4C
97A3-   8D 9E 06    STA   $069E
97A6-   A9 1A       LDA   #$1A
97A8-   8D 9F 06    STA   $069F
97AB-   A9 20       LDA   #$20
97AD-   8D A0 06    STA   $06A0

; continue the boot
97B0-   4C 00 06    JMP   $0600

; callback #8 (executed at $201A) --
; turn off drive motor and reboot to
; the work disk
97B3-   AD E8 C0    LDA   $C0E8
97B6-   4C 00 C5    JMP   $C500

*BSAVE TRACE9,A$9600,L$1B9
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ2.0400-04FF,A$2400,L$100
]CALL -151

*FC58G N 400<2400.24FFM

                 --v--

               00/00/00 00000000
               00/00/00 00000000






       00/00/00 00000000              0
       00/00/00 00000000              0






0/00/00 00000000               00/00/00
0/00/00 00000000               00/00/00

                 --^--

Those are... high scores. The blank
"00/00/00" date is the giveaway. I knew
the game had high scores (visible by
pressing "S" during the attract mode),
but it seems to be loading and saving
them on disk. Or at least loading them.
The original disk had no write-enable
notch, so it can't be saving them. But
after a game ends, the disk drive light
does turn on briefly. Maybe it's trying
to save high scores and silently
failing?

The answer, as before, is already on
our work disk. We just weren't looking
in the right place.

Earlier (much earlier!) in the boot,
we copied code to $0200 and $0300, but
we never seem to use it.

; copy page $05 to page $02 and most of
; page $06 to page $03 (but don't
; overwrite the vectors at the end of
; page $03 like the reset vector)
24A3-   A0 00       LDY   #$00
24A5-   B9 00 05    LDA   $0500,Y
24A8-   99 00 02    STA   $0200,Y
24AB-   C0 F0       CPY   #$F0
24AD-   B0 06       BCS   $24B5
24AF-   B9 00 06    LDA   $0600,Y
24B2-   99 00 03    STA   $0300,Y
24B5-   C8          INY
24B6-   D0 ED       BNE   $24A5

It's time to revisit that code.

*BLOAD OBJ.0400-07FF,A$2400

*2500L

; data? this ends up at $0200
2500-   00          BRK
2501-   63          ???
2502-   60          RTS
2503-   40          RTI
2504-   42          ???
2505-   00          BRK
2506-   00          BRK

; looks like an entry point at what
; would be $0207
2507-   AE 02 02    LDX   $0202
250A-   BD 89 C0    LDA   $C089,X

; wait for drive to spin up (not shown)
250D-   A9 00       LDA   #$00
250F-   20 11 03    JSR   $0311

; seek to track $21 (not shown)
2512-   A9 42       LDA   #$42
2514-   20 CD 02    JSR   $02CD

; death counter
2517-   A9 40       LDA   #$40
2519-   8D 03 02    STA   $0203
251C-   88          DEY
251D-   D0 08       BNE   $2527
251F-   CE 03 02    DEC   $0203
2522-   D0 03       BNE   $2527

; if this loop fails, exit via $02B8
2524-   4C B8 02    JMP   $02B8

; look for nibble sequence $D5 $FF $DD
2527-   BD 8C C0    LDA   $C08C,X
252A-   10 FB       BPL   $2527
252C-   C9 D5       CMP   #$D5
252E-   D0 EC       BNE   $251C
2530-   BD 8C C0    LDA   $C08C,X
2533-   10 FB       BPL   $2530
2535-   C9 FF       CMP   #$FF
2537-   D0 F3       BNE   $252C
2539-   BD 8C C0    LDA   $C08C,X
253C-   10 FB       BPL   $2539
253E-   C9 DD       CMP   #$DD
2540-   D0 F3       BNE   $2535

; if we found that on track $21, then
; the original disk is probably still
; in the drive, so seek to track $22
2542-   A9 44       LDA   #$44
2544-   20 CD 02    JSR   $02CD

; custom write routine
2547-   A0 FF       LDY   #$FF
2549-   BD 8D C0    LDA   $C08D,X
254C-   BD 8E C0    LDA   $C08E,X
254F-   9D 8F C0    STA   $C08F,X
2552-   1D 8C C0    ORA   $C08C,X
2555-   A9 80       LDA   #$80
2557-   20 11 03    JSR   $0311
255A-   20 11 03    JSR   $0311
255D-   BD 8D C0    LDA   $C08D,X
2560-   BD 8E C0    LDA   $C08E,X
2563-   98          TYA
2564-   9D 8F C0    STA   $C08F,X
2567-   1D 8C C0    ORA   $C08C,X
256A-   48          PHA
256B-   68          PLA
256C-   C1 00       CMP   ($00,X)
256E-   C1 00       CMP   ($00,X)
2570-   EA          NOP
2571-   C8          INY
2572-   9D 8D C0    STA   $C08D,X
2575-   1D 8C C0    ORA   $C08C,X
2578-   B9 BE 02    LDA   $02BE,Y
257B-   D0 EF       BNE   $256C
257D-   A8          TAY
257E-   EA          NOP
257F-   EA          NOP

; specifically, we're writing what's at
; $0700..$07FF
2580-   B9 00 07    LDA   $0700,Y
2583-   48          PHA
2584-   4A          LSR
2585-   09 AA       ORA   #$AA
2587-   9D 8D C0    STA   $C08D,X
258A-   DD 8C C0    CMP   $C08C,X
258D-   C1 00       CMP   ($00,X)
258F-   EA          NOP
2590-   EA          NOP
2591-   48          PHA
2592-   68          PLA
2593-   68          PLA
2594-   09 AA       ORA   #$AA
2596-   9D 8D C0    STA   $C08D,X
2599-   DD 8C C0    CMP   $C08C,X
259C-   48          PHA
259D-   68          PLA
259E-   C8          INY
259F-   D0 DF       BNE   $2580

; write a 1-nibble epilogue ($D5)
25A1-   A9 D5       LDA   #$D5
25A3-   C1 00       CMP   ($00,X)
25A5-   EA          NOP
25A6-   EA          NOP
25A7-   9D 8D C0    STA   $C08D,X
25AA-   1D 8C C0    ORA   $C08C,X
25AD-   A9 08       LDA   #$08
25AF-   20 11 03    JSR   $0311
25B2-   BD 8E C0    LDA   $C08E,X
25B5-   BD 8C C0    LDA   $C08C,X

; failure path (from $0224) also ends
; up here --
; turn off the drive motor and exit
; gracefully (the caller doesn't get
; any indication of whether the write
; succeeded)
25B8-   BD 88 C0    LDA   $C088,X
25BB-   EA          NOP
25BC-   EA          NOP
25BD-   60          RTS

As I said, this routine never succeeds
on the original disk, because the disk
is write-protected. But also, this was
a "turnkey" copy protection scheme that
provided both a fast booter, a custom
title screen, and loading/saving high
scores.

All of which leads to one inevitable
conclusion: my crack should load and
save high scores.

                   ~

              Chapter 13
   In Which We Hatch A Cunning Plan


I've captured everything this game
reads (and writes). Now the challenge
is to reconstruct the boot process on a
standard disk.

The title screen is not a problem, but
after that returns, the bootloader is
expected to load almost all of main
memory -- $0800..$BFFF, plus one page
at $0400..$04FF for the high scores,
minus hi-res page 1 at $2000..$3FFF,
plus all the last-minute changes to
zero page before jumping to the game
entry point. Hi-res page 1 is visible
at this point, so we can't put any code
there. The only available memory for
our own code is below $0800, unless
we're willing to raise the minimum
memory requirements to 64K and put our
code in the language card. (No.)

Not much room to work with, and that
doesn't even include implementing a
16-sector write routine from scratch.

On the bright side, we can leave out a
few things -- the ROM check at $6366,
the unfriendly reset vectors on page 3,
and all the Badlands code that wipes
memory when you press Ctrl-Reset. Also,
the write (and seek) routines are the
only "persistent" code that is called
after the initial boot. After we load
the game code and the high scores, we
no longer use the disk read routines
at all. If we decouple those from the
write and seek routines, we can save
some space.

Thus:

For the first stage of the boot, we get
the drive firmware to load T00,S00 at
at $0800 and jump to $0801, like every
other disk. From there, we can reuse
the drive firmware to load three more
sectors into $0900..$0BFF. That will
comprise enough code to read the rest
of the disk (without reusing the drive
firmware), as well as providing the
persistent write and seek routines that
the game can call later.

Like the original disk, we'll clear
hi-res page 1 and display it, so the
screen appears to be blank while we
copy the rest of our boot code into the
text page. Since we're done reusing the
drive firmware, we can safely copy our
write and seek routines down to pages 2
and 3. (Page 3 contains data tables
used by the firmware during boot, so we
can't use that firmware to read sectors
directly into low memory.)

Then we'll initialize and call our own
fast read routine to read the title
screen and associated code into
$2000..$5FFF. The original disk loaded
four more pages above that, but that
was all devoted to the ROM checks and
other copy protection-related code that
we won't be using. (Sorry, Roland.)

The title animation routine starts at
$4000 and expects to return to the
bootloader by setting the stack pointer
to #$40. If we set up $0141/$0142 to
point to our own code, we'll get the
full game loading experience without
changing a single byte of game code.
This pleases me.

Once we regain control from the title
screen, we can reuse our fast read
routine (still in the text page) to
load the game code into $0800..$BFFF,
seek to track $22, and read one sector
into $0400 for the high scores. We'll
reproduce all the last-minute changes
to zero page and page 2, and jump to
the game entry point via JMP ($0020).

Mirabile dictu.

The disk map will look like this:

track | memory range
------+--------------------------------
 $00  | $0800..$0BFF (bootloader)

 $01  | $2000..$2FFF \
 $02  | $3000..$3FFF  \ title page
 $03  | $4000..$4FFF  /
 $04  | $5000..$5FFF /
      |
 $05  | $4000..$4FFF \
 $06  | $5000..$5FFF  \
 $07  | $6000..$6FFF   \
 $08  | $7000..$7FFF    \
 $09  | $8000..$8FFF     \ game code
 $0A  | $9000..$9FFF     /
 $0B  | $A000..$AFFF    /
 $0C  | $B000..$BFFF   /
 $0D  | $0800..$17FF  /
 $0E  | $1800..$1FFF /
      |
0F-21 | [unused]
      |
 $22  | $0400..$04FF - high scores

Mirabile visu.

This will write the title page and
animation routine to tracks $01-$04:

*B800L

; page count (decremented)
B800-   A9 40       LDA   #$40
B802-   85 FF       STA   $FF

; logical sector (incremented)
B804-   A9 00       LDA   #$00
B806-   85 FE       STA   $FE

; call RWTS to write sector
B808-   A9 B8       LDA   #$B8
B80A-   A0 88       LDY   #$88
B80C-   20 D9 03    JSR   $03D9

; increment logical sector, wrap around
; from $0F to $00 and increment track
B80F-   E6 FE       INC   $FE
B811-   A4 FE       LDY   $FE
B813-   C0 10       CPY   #$10
B815-   D0 07       BNE   $B81E
B817-   A0 00       LDY   #$00
B819-   84 FE       STY   $FE
B81B-   EE 8C B8    INC   $B88C

; convert logical to physical sector
B81E-   B9 40 B8    LDA   $B840,Y
B821-   8D 8D B8    STA   $B88D

; increment page to write
B824-   EE 91 B8    INC   $B891

; loop until done with all pages
B827-   C6 FF       DEC   $FF
B829-   D0 DD       BNE   $B808
B82B-   60          RTS

*B840.B84F

; logical to physical sector mapping
B840- 00 07 0E 06 0D 05 0C 04
B848- 0B 03 0A 02 09 01 08 0F

*B888.B897

; RWTS parameter table, pre-initialized
; with slot 6, drive 1, track $01,
; sector $00, address $2000, and RWTS
; write command ($02)
B888- 01 60 01 00 01 00 FB F7
B890- 00 20 00 00 02 00 00 60

*BSAVE WRITER1,A$B800,L$98

*BLOAD OBJ.2000-63FF,A$2000

[S6,D1=blank disk]

*B800G

Boom, tracks $01-$04 have the title
page and animation routine.

Similarly, WRITER2 writes the game code
at $4000..$BFFF to tracks $05-$0C.

B800-   A9 80       LDA   #$80
B802-   85 FF       STA   $FF
.
.
.
B888- 01 60 01 00 05 00 FB F7
B890- 00 10 00 00 02 00 00 60

*BSAVE WRITER2,A$B800,L$98,S5,D1
*BLOAD OBJ2.4000-BEFF,A$1000
*BLOAD OBJ2.BF00-BFFF,A$8F00
*B800G

Boom, tracks $05-$0C have game code.

WRITER3 writes the rest of the game
code (at $0800..$1FFF) to tracks $0D
and $0E.

B800-   A9 18       LDA   #$18
B802-   85 FF       STA   $FF
.
.
.
B888- 01 60 01 00 0D 00 FB F7
B890- 00 08 00 00 02 00 00 60

*BSAVE WRITER3,A$B800,L$98,S5,D1
*BLOAD OBJ2.0800-08FF,A$800
*BLOAD OBJ2.0900-1FFF,A$900
*B800G

Boom, tracks $0D-$0E have the rest of
the game code.

Finally, WRITER4 writes the high scores
to track $22, sector 0.

B800-   A9 01       LDA   #$01
B802-   85 FF       STA   $FF
.
.
.
B888- 01 60 01 00 22 00 FB F7
B890- 00 24 00 00 02 00 00 60

*BSAVE WRITER4,A$B800,L$98,S5,D1
*BLOAD OBJ2.0400-04FF,A$2400
*B800G

Boom. All the game code is in place.

Now we get to write an RWTS.

                   ~

              Chapter 14
          Introducing EggBoot


EggBoot is a fast bootloader and full
read/write RWTS. It fits in 4 sectors
on track 0, including a boot sector. It
uses only 6 pages of memory for all its
code + data + scratch space. It uses no
zero page addresses after boot. It can
fill main memory in 3 seconds (not a
typo). That's twice as fast as the
original disk.

EggBoot is based on qkumba's "qboot,"
which he wrote from scratch because of
course he did. I adapted it to fit the
unique constraints of this game, which
was the easy part.

After boot-time initialization, EggBoot
is dead simple and ready to use:

entry | command | parameters
------+---------+----------------------
$0500 | read    | A = first track
      |         | Y = first page
      |         | X = sector count
------+---------+----------------------
$0207 | write   | none (always reads
      |         | from $0700 and writes
      |         | to T22,S00)
------+---------+----------------------
$0300 | seek    | A = track

That's it. It's so small, there's a
whopping 61 unused bytes in the boot
sector. You could fit a cute message in
there! (I didn't.)

Some important notes:

- The read routine reads consecutive
  tracks in physical sector order into
  consecutive pages in memory. There
  is no translation from physical to
  logical sectors. This is why we
  converted logical to physical sectors
  when we wrote the game code to disk.

- The write routine writes one sector,
  and always the same one: T22,S00,
  with the data at $0700. This data is
  provided by the game before calling.

- The seek routine can seek forward or
  back to any whole track. (I mention
  this because some fastloaders can
  only seek forward.)

I said EggBoot takes 6 pages in memory,
but I've only mentioned 3. The other 3
are for data:

$01AA..$01FF - scratch space for read
  (we manipulate the stack pointer to
  ensure this space is available)
$0500..$0555 - scratch space for write
  (this space is unused at the exact
  moment we write high scores to disk,
  since the game doesn't use it and our
  boot-time read routine is long gone
  by then)
$0600..$06FF and
$0796..$07FF - data tables for read
  (initialized during boot, only used
  until we pass control to the game)

                   ~

              Chapter 15
             EggBoot Boot0


EggBoot starts, as all disks start, on
track $00. Sector $00 (boot0) reuses
the disk controller ROM routine to read
sector $0E, $0D, and $0C (boot1). Then
we create a few data tables, modify the
boot1 code to accommodate booting from
any slot, and jump to it.

; tell the ROM to load only this sector
; (we'll do the rest manually)
0800-  [01]

; Zero page $27 is set to the next page
; after the page we just read, so #$09
; after reading into $0800, &c. We'll
; reuse the drive firmware to read
; three more sectors into $0900, $0A00,
; and $0B00.
0801-   A5 27       LDA   $27
0803-   C9 0C       CMP   #$0C
0805-   B0 0E       BCS   $0815

; Set up next sector number to read.
; The disk controller ROM does this
; once already, but due to quirks of
; timing, it's much faster to increment
; it twice so the next sector you want
; to load is actually the next sector
; under the drive head. Otherwise you
; end up waiting for the disk to spin
; an entire revolution, which is quite
; slow.
0807-   E6 3D       INC   $3D

; Set up the "return" address to jump
; to the "read sector" entry point of
; the disk controller ROM. This could
; be anywhere in $Cx00 depending on the
; slot we booted from, which is why we
; put the boot slot in the accumulator
; at $0808.
0809-   8A          TXA
080A-   4A          LSR
080B-   4A          LSR
080C-   4A          LSR
080D-   4A          LSR
080E-   09 C0       ORA   #$C0

; push the entry point on the stack
0810-   48          PHA
0811-   A9 5B       LDA   #$5B
0813-   48          PHA

; "Return" to the entry point via RTS.
; The disk controller ROM always jumps
; to $0801 (remember, that's why we
; had to move it and patch it to trace
; the boot all the way back in chapter
; 1), so this entire thing is a loop
; that only exits via the "BCS" branch
; at $0805.
0814-   60          RTS

; Execution continues here (from $0805)
; after three sectors have been loaded
; into memory at $0900..$0BFF.
0815-   20 8A 0B    JSR   $0B8A

*B8AL

; clear hi-res page 1
0B8A-   A2 20       LDX   #$20
0B8C-   A0 00       LDY   #$00
0B8E-   98          TYA
0B8F-   99 00 20    STA   $2000,Y
0B92-   C8          INY
0B93-   D0 FA       BNE   $0B8F
0B95-   EE 91 0B    INC   $0B91
0B98-   CA          DEX
0B99-   D0 F4       BNE   $0B8F

; display hi-res page 1
0B9B-   AD 57 C0    LDA   $C057
0B9E-   AD 52 C0    LDA   $C052
0BA1-   AD 54 C0    LDA   $C054
0BA4-   AD 50 C0    LDA   $C050

; move EggBoot to lower memory
0BA7-   B9 00 09    LDA   $0900,Y
0BAA-   99 00 05    STA   $0500,Y
0BAD-   B9 00 0A    LDA   $0A00,Y
0BB0-   99 00 02    STA   $0200,Y
0BB3-   B9 7F 08    LDA   $087F,Y
0BB6-   99 00 07    STA   $0700,Y
0BB9-   C0 8A       CPY   #$8A
0BBB-   B0 06       BCS   $0BC3
0BBD-   B9 00 0B    LDA   $0B00,Y
0BC0-   99 00 03    STA   $0300,Y
0BC3-   C8          INY
0BC4-   D0 E1       BNE   $0BA7

; There are a number of places in boot1
; that hit a slot-specific soft switch
; (read a nibble from disk, turn off
; the drive, &c). Rather than the usual
; form of "LDA $C08C,X", we will use
; "LDA $C0EC" and modify the $EC byte
; in advance, based on the boot slot.
0BC6-   A5 2B       LDA   $2B
0BC8-   09 8C       ORA   #$8C
0BCA-   8D 70 05    STA   $0570
0BCD-   8D 82 05    STA   $0582
0BD0-   8D 9A 05    STA   $059A
0BD3-   8D B2 05    STA   $05B2
0BD6-   8D C8 05    STA   $05C8
0BD9-   8D F6 02    STA   $02F6

; munge $EC -> $E8 (used later to turn
; off the drive motor)
0BDC-   29 F8       AND   #$F8
0BDE-   8D F9 05    STA   $05F9

; munge $E8 -> $E9 (used later to turn
; on the drive motor)
0BE1-   09 01       ORA   #$01
0BE3-   8D 79 03    STA   $0379
0BE6-   8D 17 02    STA   $0217

; munge $E9 -> $E0 (used later to move
; the drive head via the stepper motor)
0BE9-   49 09       EOR   #$09
0BEB-   8D 57 03    STA   $0357

; munge $E0 -> $60 (boot slot x16, used
; by the write routine)
0BEE-   29 70       AND   #$70
0BF0-   8D 47 02    STA   $0247
0BF3-   8D 79 02    STA   $0279
0BF6-   8D 8F 02    STA   $028F
0BF9-   8D BD 02    STA   $02BD
0BFC-   60          RTS

                   ~

              Chapter 16
                 6 + 2


Before I dive into the next chunk of
code, I get to pause and explain a
little bit of theory. As you probably
know if you're the sort of person who's
read this far already, Apple II floppy
disks do not contain the actual data
that ends up being loaded into memory.
Due to hardware limitations of the
original Disk II drive, data on disk is
stored in an intermediate format called
"nibbles." Bytes in memory are encoded
into nibbles before writing to disk,
and nibbles that you read from the disk
must be decoded back into bytes. The
round trip is lossless but requires
some bit wrangling.

Decoding nibbles-on-disk into bytes-in-
memory is a multi-step process. In
"6-and-2 encoding" (used by DOS 3.3,
ProDOS, and all ".dsk" image files),
there are 64 possible values that you
may find in the data field (in the
range $96..$FF, but not all of those,
because some of them have bit patterns
that trip up the drive firmware). We'll
call these "raw nibbles."

Step 1: read $156 raw nibbles from the
data field. These values will range
from $96 to $FF, but as mentioned
earlier, not all values in that range
will appear on disk.

Now we have $156 raw nibbles.

Step 2: decode each of the raw nibbles
into a 6-bit byte between 0 and 63
(%00000000 and %00111111 in binary).
$96 is the lowest valid raw nibble, so
it gets decoded to 0. $97 is the next
valid raw nibble, so it's decoded to 1.
$98 and $99 are invalid, so we skip
them, and $9A gets decoded to 2. And so
on, up to $FF (the highest valid raw
nibble), which gets decoded to 63.

Now we have $156 6-bit bytes.

Step 3: split up each of the first $56
6-bit bytes into pairs of bits. In
other words, each 6-bit byte becomes
three 2-bit bytes. These 2-bit bytes
are merged with the next $100 6-bit
bytes to create $100 8-bit bytes. Hence
the name, "6-and-2" encoding.

The exact process of how the bits are
split and merged is... complicated. The
first $56 6-bit bytes get split up into
2-bit bytes, but those two bits get
swapped (so %01 becomes %10 and vice-
versa). The other $100 6-bit bytes each
get multiplied by 4 (a.k.a. bit-shifted
two places left). This leaves a hole in
the lower two bits, which is filled by
one of the 2-bit bytes from the first
group.

A diagram might help. "a" through "x"
each represent one bit.

             -------------

1 decoded      3 decoded
nibble in  +   nibbles in   =  3 bytes
first $56      other $100


00abcdef       00ghijkl
               00mnopqr
   |           00stuvwx
   |
 split            |
   &           shifted
swapped        left x2
   |              |
   V              V

000000fe   +   ghijkl00   =   ghijklfe
000000dc   +   mnopqr00   =   mnopqrdc
000000ba   +   stuvwx00   =   stuvwxba

             -------------

Tada! Four 6-bit bytes

  00abcdef
  00ghijkl
  00mnopqr
  00stuvwx

become three 8-bit bytes

  ghijklfe
  mnopqrdc
  stuvwxba

When DOS 3.3 reads a sector, it reads
the first $56 raw nibbles, decoded them
into 6-bit bytes, and stashes them in a
temporary buffer (at $BC00). Then it
reads the other $100 raw nibbles,
decodes them into 6-bit bytes, and puts
them in another temporary buffer (at
$BB00). Only then does DOS 3.3 start
combining the bits from each group to
create the full 8-bit bytes that will
end up in the target page in memory.
This is why DOS 3.3 "misses" sectors
when it's reading, because it's busy
twiddling bits while the disk is still
spinning.

EggBoot also uses "6-and-2" encoding.
The first $56 nibbles in the data field
are still split into pairs of bits that
will be merged with nibbles that won't
come until later. But instead of
waiting for all $156 raw nibbles to be
read from disk, it "interleaves" the
nibble reads with the bit twiddling
required to merge the first $56 6-bit
bytes and the $100 that follow. By the
time EggBoot gets to the data field
checksum, it has already stored all
$100 8-bit bytes in their final resting
place in memory. This means that we can
read all 16 sectors on a track in one
revolution of the disk. That's what
makes it crazy fast.

To make it possible to twiddle the bits
and not miss nibbles as the disk
spins(*), we do some of the work in
advance. We multiply each of the 64
possible decoded values by 4 and store
those values. (Since this is done by
bit shifting and we're doing it before
we start reading the disk, this is
called the "pre-shift" table.) We also
store all possible 2-bit values in a
repeating pattern that will make it
easy to look them up later. Then, as
we're reading from disk (and timing is
tight), we can simulate bit math with a
series of table lookups. There is just
enough time to convert each raw nibble
into its final 8-bit byte before
reading the next nibble.

(*) The disk spins independently of the
    CPU, and we only have a limited
    time to read a nibble and do what
    we're going to do with it before
    WHOOPS HERE COMES ANOTHER ONE. So
    time is of the essence. Also, "As
    The Disk Spins" would make a great
    name for a retrocomputing-themed
    soap opera.

The first table, at $0600..$06FF, is
three columns wide and 64 rows deep.
Astute readers will notice that 3 x 64
is not 256. Only three of the columns
are used; the fourth (unused) column
exists because multiplying by 3 is hard
but multiplying by 4 is easy (in base 2
anyway). The three columns correspond
to the three pairs of 2-bit values in
those first $56 6-bit bytes. Since the
values are only 2 bits wide, each
column holds one of four different
values (%00, %01, %10, or %11).

The second table, at $0796..$07FF, is
the "pre-shift" table. This contains
all the possible 6-bit bytes, in order,
each multiplied by 4 (a.k.a. shifted to
the left two places, so the 6 bits that
started in columns 0-5 are now in
columns 2-7, and columns 0 and 1 are
zeroes). Like this:

       00ghijkl   -->   ghijkl00

Astute readers will notice that there
are only 64 possible 6-bit bytes, but
this second table is larger than 64
bytes. To make lookups easier, the
table has empty slots for each of the
invalid raw nibbles. In other words, we
don't do any math to decode raw nibbles
into 6-bit bytes; we just look them up
in this table (offset by $96, since
that's the lowest valid raw nibble) and
get the required bit shifting for free.


 addr | raw | decoded 6-bit | pre-shift
------+-----+---------------+----------
$0796 | $96 | 0 = %00000000 | %00000000
$0797 | $97 | 1 = %00000001 | %00000100
$0798 | $98       [invalid raw nibble]
$0799 | $99       [invalid raw nibble]
$079A | $9A | 2 = %00000010 | %00001000
$079B | $9B | 3 = %00000011 | %00001100
$079C | $9C       [invalid raw nibble]
$079D | $9D | 4 = %00000100 | %00010000
  .
  .
  .
$07FE | $FE | 62 = %00111110 | %11111000
$07FF | $FF | 63 = %00111111 | %11111100


Each value in this "pre-shift" table
also serves as an index into the first
table (with all the 2-bit bytes). This
wasn't an accident; I mean, that sort
of magic doesn't just happen. But the
table of 2-bit bytes is arranged in
such a way that we can take one of the
raw nibbles to be decoded and split
apart (from the first $56 raw nibbles
in the data field), use each raw nibble
as an index into the pre-shift table,
then use that pre-shifted value as an
index into the first table to get the
2-bit value at exactly the right time.

                   ~

              Chapter 17
            Back to EggBoot


Continuing from $0818...

This is the loop that creates the
pre-shift table at $0796. As a special
bonus, it also creates the inverse
table that is used during disk write
operations (converting in the other
direction).

0818-   A2 3F       LDX   #$3F
081A-   86 FF       STX   $FF
081C-   E8          INX
081D-   A0 7F       LDY   #$7F
081F-   84 FE       STY   $FE
0821-   98          TYA
0822-   0A          ASL
0823-   24 FE       BIT   $FE
0825-   F0 18       BEQ   $083F
0827-   05 FE       ORA   $FE
0829-   49 FF       EOR   #$FF
082B-   29 7E       AND   #$7E
082D-   B0 10       BCS   $083F
082F-   4A          LSR
0830-   D0 FB       BNE   $082D
0832-   CA          DEX
0833-   8A          TXA
0834-   0A          ASL
0835-   0A          ASL
0836-   99 80 07    STA   $0780,Y
0839-   98          TYA
083A-   09 80       ORA   #$80
083C-   9D 8A 03    STA   $038A,X
083F-   88          DEY
0840-   D0 DD       BNE   $081F

And this is the result (".." means the
address is uninitialized and unused):

0790-                   00 04
0798- .. .. 08 0C .. 10 14 18
07A0- .. .. .. .. .. .. 1C 20
07A8- .. .. .. 24 28 2C 30 34
07B0- .. .. 38 3C 40 44 48 4C
07B8- .. 50 54 58 5C 60 64 68
07C0- .. .. .. .. .. .. .. ..
07C8- .. .. .. 6C .. 70 74 78
07D0- .. .. .. 7C .. .. 80 84
07D8- .. 88 8C 90 94 98 9C A0
07E0- .. .. .. .. .. A4 A8 AC
07E8- .. B0 B4 B8 BC C0 C4 C8
07F0- .. .. CC D0 D4 D8 DC E0
07F8- .. E4 E8 EC F0 F4 F8 FC

Next up: a loop to create the table of
2-bit values at $0600, magically
arranged to enable easy lookups later.

0842-   84 FD       STY   $FD
0844-   46 FF       LSR   $FF
0846-   46 FF       LSR   $FF
0848-   BD D8 05    LDA   $05D8,X
084B-   99 00 06    STA   $0600,Y
084E-   E6 FD       INC   $FD
0850-   A5 FD       LDA   $FD
0852-   25 FF       AND   $FF
0854-   D0 05       BNE   $085B
0856-   E8          INX
0857-   8A          TXA
0858-   29 03       AND   #$03
085A-   AA          TAX
085B-   C8          INY
085C-   C8          INY
085D-   C8          INY
085E-   C8          INY
085F-   C0 03       CPY   #$03
0861-   B0 E5       BCS   $0848
0863-   C8          INY
0864-   C0 03       CPY   #$03
0866-   90 DC       BCC   $0844

And this is the result:

0600- 00 00 00 .. 00 00 02 ..
0608- 00 00 01 .. 00 00 03 ..
0610- 00 02 00 .. 00 02 02 ..
0618- 00 02 01 .. 00 02 03 ..
0620- 00 01 00 .. 00 01 02 ..
0628- 00 01 01 .. 00 01 03 ..
0630- 00 03 00 .. 00 03 02 ..
0638- 00 03 01 .. 00 03 03 ..
0640- 02 00 00 .. 02 00 02 ..
0648- 02 00 01 .. 02 00 03 ..
0650- 02 02 00 .. 02 02 02 ..
0658- 02 02 01 .. 02 02 03 ..
0660- 02 01 00 .. 02 01 02 ..
0668- 02 01 01 .. 02 01 03 ..
0670- 02 03 00 .. 02 03 02 ..
0678- 02 03 01 .. 02 03 03 ..
0680- 01 00 00 .. 01 00 02 ..
0688- 01 00 01 .. 01 00 03 ..
0690- 01 02 00 .. 01 02 02 ..
0698- 01 02 01 .. 01 02 03 ..
06A0- 01 01 00 .. 01 01 02 ..
06A8- 01 01 01 .. 01 01 03 ..
06B0- 01 03 00 .. 01 03 02 ..
06B8- 01 03 01 .. 01 03 03 ..
06C0- 03 00 00 .. 03 00 02 ..
06C8- 03 00 01 .. 03 00 03 ..
06D0- 03 02 00 .. 03 02 02 ..
06D8- 03 02 01 .. 03 02 03 ..
06E0- 03 01 00 .. 03 01 02 ..
06E8- 03 01 01 .. 03 01 03 ..
06F0- 03 03 00 .. 03 03 02 ..
06F8- 03 03 01 .. 03 03 03 ..

And with that, EggBoot is fully armed
and operational.

; Set up an initial read of $40 sectors
; from track $01 into $2000..$5FFF.
; Also set the stack pointer lower,
; because the read routine uses the top
; $56 bytes of the stack page as a
; temporary buffer. (Hey, I told you
; memory was tight.)
0868-   A9 01       LDA   #$01
086A-   A2 40       LDX   #$40
086C-   9A          TXS
086D-   A0 20       LDY   #$20

; Read the title page and animation
; routine from tracks $01-$05
086F-   20 00 05    JSR   $0500

; Set up the "return" address at $0141/
; $0142 to regain control after the
; title page code is done.
0872-   A9 FF       LDA   #$FF
0874-   8D 41 01    STA   $0141
0877-   A9 06       LDA   #$06
0879-   8D 42 01    STA   $0142

; jump to the title page entry point
087C-   4C 00 40    JMP   $4000

Execution will continue at $0700 (which
is $06FF+1) when the title page routine
returns. But first, I get to finish
showing you how the disk read routine
works.

                   ~

              Chapter 18
            Read & Go Seek


In a standard DOS 3.3 RWTS, the
softswitch to read the data latch is
"LDA $C08C,X", where X is the boot slot
times 16 (to allow disks to boot from
any slot). EggBoot also supports
booting and reading from any slot, but
instead of using an index, most fetch
instructions are set up in advance
based on the boot slot. Not only does
this free up the X register, it lets us
juggle all the registers and put the
raw nibble value in whichever one is
convenient at the time. (We take full
advantage of this freedom.) I've marked
each pre-set softswitch with "o_O".

There are several other instances of
addresses and constants that get
modified while EggBoot is executing.
I've left these with a bogus value $D1
and marked them with "o_O".

EggBoot's source code should be
available from the same place you found
this write-up. If you're looking to
modify this code for your own purposes,
I suggest you "use the source, Luke."

; A = the track number to seek to. We
; multiply it by 2 to convert it to a
; phase, then store it inside the seek
; routine which we will call shortly.
0500-   0A          ASL
0501-   8D 13 03    STA   $0313

; X = the number of sectors to read
0504-   8E C8 05    STX   $05C8

; Y = the starting address in memory
0507-   8C 21 05    STY   $0521

; turn on the drive motor and wait for
; it to spin up (not shown)
050A-   20 78 03    JSR   $0378

; are we reading this entire track?
050D-   A9 10       LDA   #$10
050F-   CD C8 05    CMP   $05C8

; yes -> branch
0512-   B0 01       BCS   $0515

; no -> store the number of sectors we
; want to read
0514-   AA          TAX
0515-   8E D8 05    STX   $05D8

; seek to the track we want to read
; (not shown)
0518-   20 07 03    JSR   $0307

; Initialize an array of which sectors
; we've read from the current track.
; The array is in physical sector
; order, thus the RWTS assumes data is
; stored in physical sector order on
; each track.  (This saves 18 bytes: 16
; for the table and 2 for the lookup
; command!) Values are the actual pages
; in memory where that sector should
; go, and they get zeroed once the
; sector is read (so we don't waste
; time decoding the same sector twice).
051B-   AE D8 05    LDX   $05D8
051E-   A0 00       LDY   #$00
0520-   A9 D1       LDA   #$D1      o_O
0522-   99 D9 05    STA   $05D9,Y
0525-   EE 21 05    INC   $0521
0528-   C8          INY
0529-   CA          DEX
052A-   D0 F4       BNE   $0520

052C-   20 E6 02    JSR   $02E6

; This routine reads nibbles from disk
; until it finds the sequence "D5 AA",
; then it reads one more nibble and
; returns it in the accumulator. We
; reuse this routine to find both the
; address and data field prologues.
02E6-   20 F5 02    JSR   $02F5
02E9-   C9 D5       CMP   #$D5
02EB-   D0 F9       BNE   $02E6
02ED-   20 F5 02    JSR   $02F5
02F0-   C9 AA       CMP   #$AA
02F2-   D0 F5       BNE   $02E9
02F4-   A8          TAY
02F5-   AD EC C0    LDA   $C0EC     o_O
02F8-   10 FB       BPL   $02F5
02FA-   60          RTS

Continuing from $052F...

; If that third nibble is not #$AD, we
; assume it's the end of the address
; prologue. (#$96 would be the third
; nibble of a standard address
; prologue, but we don't actually
; check.) We fall through and start
; decoding the 4-4 encoded values in
; the address field.
052F-   49 AD       EOR   #$AD
0531-   F0 1A       BEQ   $054D

0533-   20 D3 02    JSR   $02D3

; This routine parses the 4-4-encoded
; values in the address field. The
; first time through this loop, we'll
; read the disk volume number. The
; second time, we'll read the track
; number. The third time, we'll read
; the physical sector number. We don't
; actually care about the disk volume
; or the track number, and once we get
; the sector number, we don't verify
; the address field checksum.
02D3-   A0 03       LDY   #$03
02D5-   20 F5 02    JSR   $02F5
02D8-   2A          ROL
02D9-   8D B9 05    STA   $05B9
02DC-   20 F5 02    JSR   $02F5
02DF-   2D B9 05    AND   $05B9
02E2-   88          DEY
02E3-   D0 F0       BNE   $02D5

; On exit, the accumulator contains the
; physical sector number.
02E5-   60          RTS

Continuing from $0536...

; use physical sector number as an
; index into the sector address array
0536-   A8          TAY

; get the target page (where we want to
; store this sector in memory)
0537-   BE D9 05    LDX   $05D9,Y

; if the target page is #$00, it means
; we've already read this sector, so
; loop back to find the next address
; prologue
053A-   F0 F0       BEQ   $052C

; store the physical sector number
; later in this routine
053C-   8D B9 05    STA   $05B9

; store the target page in several
; places throughout this routine
053F-   8E A6 05    STX   $05A6
0542-   CA          DEX
0543-   8E 76 05    STX   $0576
0546-   8E 8E 05    STX   $058E
0549-   A0 00       LDY   #$00

; this is an unconditional branch
054B-   B0 DF       BCS   $052C

; execution continues here (from $0531)
; after matching the data prologue
054D-   E0 00       CPX   #$00

; If X is still #$00, it means we found
; a data prologue before we found an
; address prologue. In that case, we
; skip this sector, because we don't
; know which sector it is and we
; wouldn't know where to put it. Sad!
054F-   F0 DB       BEQ   $052C

Nibble loop #1 reads nibbles $00..$55,
looks up the corresponding offset in
the preshift table at $0796, and stores
that offset in a temporary buffer on
the stack page.

; initialize rolling checksum to #$00,
; or update it with the results from
; the calculations below
0551-   8D 60 05    STA   $0560

; read one nibble from disk
0554-   AE EC C0    LDX   $C0EC     o_O
0557-   10 FB       BPL   $0554

; The nibble value is in the X register
; now. The lowest possible nibble value
; is $96 and the highest is $FF. To
; look up the offset in the table at
; $0796, we index off $0700 + X. Math!
0559-   BD 00 07    LDA   $0700,X

; Now the accumulator has the offset
; into the table of individual 2-bit
; combinations ($0600..$06FF). Store
; that offset in a temporary buffer
; on the stack page.
055C-   99 00 01    STA   $0100,Y

; The EOR value is set at $0551
; each time through loop #1.
055F-   49 D1       EOR   #$D1      o_O

; The Y register started at #$AA
; (set by the "TAY" instruction
; at $0536), so this loop reads
; a total of #$56 nibbles.
0561-   C8          INY
0562-   D0 ED       BNE   $0551

Here endeth nibble loop #1.

Nibble loop #2 reads nibbles $56..$AB,
combines them with bits 0-1 of the
appropriate nibble from the first $56,
and stores them in bytes $00..$55 of
the target page in memory.

0564-   A0 AA       LDY   #$AA
0566-   AE EC C0    LDX   $C0EC     o_O
0569-   10 FB       BPL   $0566
056B-   5D 00 07    EOR   $0700,X
056E-   BE 00 01    LDX   $0100,Y
0571-   5D 02 06    EOR   $0602,X

; This address was set at $0543
; based on the target page (minus 1
; so we can add Y from #$AA..#$FF).
0574-   99 56 D1    STA   $D156,Y   o_O
0577-   C8          INY
0578-   D0 EC       BNE   $0566

Here endeth nibble loop #2.

Nibble loop #3 reads nibbles $AC..$101,
combines them with bits 2-3 of the
appropriate nibble from the first $56,
and stores them in bytes $56..$AB of
the target page in memory.

057A-   29 FC       AND   #$FC
057C-   A0 AA       LDY   #$AA
057E-   AE EC C0    LDX   $C0EC     o_O
0581-   10 FB       BPL   $057E
0583-   5D 00 07    EOR   $0700,X
0586-   BE 00 01    LDX   $0100,Y
0589-   5D 01 06    EOR   $0601,X

; This address was set at $0546
; based on the target page (minus 1
; so we can add Y from #$AA..#$FF).
058C-   99 AC D1    STA   $D1AC,Y   o_O
058F-   C8          INY
0590-   D0 EC       BNE   $057E

Here endeth nibble loop #3.

Loop #4 reads nibbles $102..$155,
combines them with bits 4-5 of the
appropriate nibble from the first $56,
and stores them in bytes $AC..$FF of
the target page in memory.

0592-   29 FC       AND   #$FC
0594-   A2 AC       LDX   #$AC
0596-   AC EC C0    LDY   $C0EC     o_O
0599-   10 FB       BPL   $0596
059B-   59 00 07    EOR   $0700,Y
059E-   BC FE 00    LDY   $00FE,X
05A1-   59 00 06    EOR   $0600,Y

; This address was set at $053F
; based on the target page.
05A4-   9D 00 D1    STA   $D100,X
05A7-   E8          INX
05A8-   D0 EC       BNE   $0596

; Finally, get the last nibble and
; convert it to a byte. This should
; equal all the previous bytes XOR'd
; together. (This is the standard
; checksum algorithm shared by all
; 16-sector disks.)
05AA-   29 FC       AND   #$FC
05AC-   AC EC C0    LDY   $C0EC     o_O
05AF-   10 FB       BPL   $05AC
05B1-   59 00 07    EOR   $0700,Y

; if data checksum failed, start over
05B4-   C9 01       CMP   #$01
05B6-   B0 93       BCS   $054B

; This was set to the physical
; sector number (at $053C), so
; this is a index into the 16-
; byte array at $05D9.
05B8-   A0 D1       LDY   #$D1      o_O
05BA-   8A          TXA

; store #$00 at this location in
; the sector array to indicate
; that we've read this sector
05BB-   99 D9 05    STA   $05D9,Y

; decrement sector count
05BE-   CE C8 05    DEC   $05C8
05C1-   CE D8 05    DEC   $05D8
05C4-   38          SEC

; If the sectors-left-in-this-track
; count (in $05D8) isn't zero yet,
; loop back to read more sectors.
05C5-   D0 EF       BNE   $05B6

; If the total sector count (in
; $05C8, set at $0504 and decremented
; at $05BE) is zero, we're done
; so we can skip the rest of
; the track. (This lets us have
; sector counts that are not
; multiples of 16, i.e. reading
; just a few sectors from the
; last track of a multi-track
; read.)
05C7-   A2 D1       LDX   #$D1      o_O
05C9-   F0 09       BEQ   $05D4

; increment track (twice because
; it's store as the phase, which
; is half a track)
05CB-   EE 13 03    INC   $0313
05CE-   EE 13 03    INC   $0313

; jump back to seek and read
; from the next track
05D1-   4C 0D 05    JMP   $050D

; Execution continues here (from
; $05C9). We're all done, so
; turn off drive motor and exit.
05D4-   AD E8 C0    LDA   $C0E8     o_O
05D7-   60          RTS

And that's all she wrote^H^H^H^Hread.

                   ~

              Chapter 19
       Omelette You Finish, But
      Roland Had One Of The Best
       Bootloaders Of All Time!


When we left our custom bootloader, we
had jumped to $4000 to let the title
page do its thing. The title page sets
the stack pointer to #$40 and returns,
so we had set $0141/$0142 to point to
the final phase of EggBoot at $0700.
This code was originally in our boot
sector and loaded at $087F, but it was
soon copied to $0700 by the subroutine
at $0B8A.

The stack pointer is already low enough
that the read routine's temporary
buffer at $01AA won't clobber actual
return addresses, so we've got that
going for us, which is nice.

; seek to track $05, read $80 sectors
; into $4000..$BFFF
0700-   A9 05       LDA   #$05
0702-   A2 80       LDX   #$80
0704-   A0 40       LDY   #$40
0706-   20 00 05    JSR   $0500

; seek to track $0D, read $18 sectors
; into $0800..$1FFF
0709-   A9 0D       LDA   #$0D
070B-   A2 18       LDX   #$18
070D-   A0 08       LDY   #$08
070F-   20 00 05    JSR   $0500

That fills main memory, $0800..$BFFF,
minus hi-res page 1 which is still
showing the title page.

; seek to track $22, read 1 sector
; into $0400
0712-   A9 22       LDA   #$22
0714-   A2 01       LDX   #$01
0716-   A0 04       LDY   #$04
0718-   20 00 05    JSR   $0500

; set game-specific zero page values
; (these were originally read from disk
; and set by the self-modifying loop at
; $06BF)
071B-   A9 80       LDA   #$80
071D-   85 20       STA   $20
071F-   A9 9F       LDA   #$9F
0721-   85 21       STA   $21
0723-   A9 00       LDA   #$00
0725-   85 3A       STA   $3A
0727-   85 4E       STA   $4E
0729-   A9 1F       LDA   #$1F
072B-   85 4F       STA   $4F
072D-   A9 0B       LDA   #$0B
072F-   85 36       STA   $36
0731-   A9 1F       LDA   #$1F
0733-   85 37       STA   $37
0735-   A9 D0       LDA   #$D0
0737-   85 38       STA   $38
0739-   A9 51       LDA   #$51
073B-   85 39       STA   $39
073D-   A9 59       LDA   #$59
073F-   85 3B       STA   $3B

; jump to game entry point
0741-   6C 20 00    JMP   ($0020)

The game uses the text page to display
high scores (and enter them, if you're
so talented), so none of this code will
survive. But that's okay, because we're
done reading the disk. Once the game is
in memory, the only disk-related
activity is writing the high scores to
track $22. That code (not shown here)
is identical to DOS 3.3's write routine
except starting at $0207. That's the
entry point the game expects, so no
changes to the game code are required.

Quod erat liberandum.

                   ~

               Changelog

2020-06-24

- typo in the 6-and-2 encoding diagram
  [thanks Andrew R.]

2019-04-14

- initial release

---------------------------------------
A 4am crack                    No. 2000
------------------EOF------------------
